mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-08-21 02:23:38 +00:00
merge: Reduce log spam (!1004)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1004 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
00c0bdbc94
113 changed files with 909 additions and 626 deletions
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
|
import { inspect } from 'node:util';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import Xev from 'xev';
|
import Xev from 'xev';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
|
@ -53,15 +54,22 @@ async function main() {
|
||||||
|
|
||||||
// Display detail of unhandled promise rejection
|
// Display detail of unhandled promise rejection
|
||||||
if (!envOption.quiet) {
|
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
|
// Display detail of uncaught exception
|
||||||
process.on('uncaughtException', err => {
|
process.on('uncaughtException', err => {
|
||||||
try {
|
try {
|
||||||
logger.error(err);
|
logger.error('Uncaught exception:', err);
|
||||||
console.trace(err);
|
} catch {
|
||||||
} catch { }
|
console.error('Uncaught exception:', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dying away...
|
// Dying away...
|
||||||
|
|
|
@ -74,7 +74,7 @@ export async function masterMain() {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
bootLogger.succ('Sharkey initialized');
|
bootLogger.info('Sharkey initialized');
|
||||||
|
|
||||||
if (config.sentryForBackend) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
@ -140,10 +140,10 @@ export async function masterMain() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (envOption.onlyQueue) {
|
if (envOption.onlyQueue) {
|
||||||
bootLogger.succ('Queue started', null, true);
|
bootLogger.info('Queue started', null, true);
|
||||||
} else {
|
} else {
|
||||||
const addressString = net.isIPv6(config.address) ? `[${config.address}]` : config.address;
|
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();
|
config = loadConfig();
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
if (typeof exception === 'string') {
|
if (typeof exception === 'string') {
|
||||||
configLogger.error(exception);
|
configLogger.error('Exception loading config:', exception);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else if ((exception as any).code === 'ENOENT') {
|
} else if ((exception as any).code === 'ENOENT') {
|
||||||
configLogger.error('Configuration file not found', null, true);
|
configLogger.error('Configuration file not found', null, true);
|
||||||
|
@ -181,7 +181,7 @@ function loadConfigBoot(): Config {
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
configLogger.succ('Loaded');
|
configLogger.info('Loaded');
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,7 @@ async function connectDb(): Promise<void> {
|
||||||
dbLogger.info('Connecting...');
|
dbLogger.info('Connecting...');
|
||||||
await initDb();
|
await initDb();
|
||||||
const v = await db.query('SHOW server_version').then(x => x[0].server_version);
|
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) {
|
} catch (err) {
|
||||||
dbLogger.error('Cannot connect', null, true);
|
dbLogger.error('Cannot connect', null, true);
|
||||||
dbLogger.error(err);
|
dbLogger.error(err);
|
||||||
|
@ -211,7 +211,7 @@ async function spawnWorkers(limit = 1) {
|
||||||
|
|
||||||
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
|
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
|
||||||
await Promise.all([...Array(workers)].map(spawnWorker));
|
await Promise.all([...Array(workers)].map(spawnWorker));
|
||||||
bootLogger.succ('All workers started');
|
bootLogger.info('All workers started');
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnWorker(): Promise<void> {
|
function spawnWorker(): Promise<void> {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { QueueService } from '@/core/QueueService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { IdService } from './IdService.js';
|
import { IdService } from './IdService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -125,11 +126,11 @@ export class AbuseReportService {
|
||||||
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
|
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
|
||||||
|
|
||||||
if (report.targetUserHost == null) {
|
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) {
|
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, {
|
await this.abuseUserReportsRepository.update(report.id, {
|
||||||
|
|
|
@ -80,9 +80,9 @@ export class BunnyService {
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', (error) => {
|
req.on('error', (error) => {
|
||||||
this.bunnyCdnLogger.error(error);
|
this.bunnyCdnLogger.error('Unhandled error', error);
|
||||||
data.destroy();
|
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.pipe(req).on('finish', () => {
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class CaptchaError extends Error {
|
||||||
public readonly cause?: unknown;
|
public readonly cause?: unknown;
|
||||||
|
|
||||||
constructor(code: CaptchaErrorCode, message: string, cause?: unknown) {
|
constructor(code: CaptchaErrorCode, message: string, cause?: unknown) {
|
||||||
super(message);
|
super(message, cause ? { cause } : undefined);
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.cause = cause;
|
this.cause = cause;
|
||||||
this.name = 'CaptchaError';
|
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 => {
|
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) {
|
if (result.success !== true) {
|
||||||
|
@ -133,7 +133,7 @@ export class CaptchaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => {
|
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) {
|
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 => {
|
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) {
|
if (result.success !== true) {
|
||||||
|
@ -386,7 +386,7 @@ export class CaptchaService {
|
||||||
this.logger.info(err);
|
this.logger.info(err);
|
||||||
const error = err instanceof CaptchaError
|
const error = err instanceof CaptchaError
|
||||||
? err
|
? err
|
||||||
: new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`);
|
: new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`, err);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error,
|
error,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DownloadService {
|
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<{
|
public async downloadUrl(url: string, path: string, options: { timeout?: number, operationTimeout?: number, maxSize?: number } = {} ): Promise<{
|
||||||
filename: string;
|
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 timeout = options.timeout ?? 30 * 1000;
|
||||||
const operationTimeout = options.operationTimeout ?? 60 * 1000;
|
const operationTimeout = options.operationTimeout ?? 60 * 1000;
|
||||||
|
@ -86,7 +87,7 @@ export class DownloadService {
|
||||||
filename = parsed.parameters.filename;
|
filename = parsed.parameters.filename;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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) => {
|
}).on('downloadProgress', (progress: Got.Progress) => {
|
||||||
|
@ -100,13 +101,17 @@ export class DownloadService {
|
||||||
await stream.pipeline(req, fs.createWriteStream(path));
|
await stream.pipeline(req, fs.createWriteStream(path));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Got.HTTPError) {
|
if (e instanceof Got.HTTPError) {
|
||||||
throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
|
throw new StatusError(`download error from ${url}`, e.response.statusCode, e.response.statusMessage, e);
|
||||||
} else {
|
} else if (e instanceof Got.RequestError || e instanceof Got.AbortError) {
|
||||||
|
throw new Error(String(e), { cause: e });
|
||||||
|
} else if (e instanceof Error) {
|
||||||
throw e;
|
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 {
|
return {
|
||||||
filename,
|
filename,
|
||||||
|
@ -118,7 +123,7 @@ export class DownloadService {
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`text file: Temp file is ${path}`);
|
this.logger.debug(`text file: Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// write content at URL to temp file
|
// write content at URL to temp file
|
||||||
|
|
|
@ -45,6 +45,7 @@ import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { BunnyService } from '@/core/BunnyService.js';
|
import { BunnyService } from '@/core/BunnyService.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { LoggerService } from './LoggerService.js';
|
import { LoggerService } from './LoggerService.js';
|
||||||
|
|
||||||
type AddFileArgs = {
|
type AddFileArgs = {
|
||||||
|
@ -202,7 +203,7 @@ export class DriveService {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Uploads
|
//#region Uploads
|
||||||
this.registerLogger.info(`uploading original: ${key}`);
|
this.registerLogger.debug(`uploading original: ${key}`);
|
||||||
const uploads = [
|
const uploads = [
|
||||||
this.upload(key, fs.createReadStream(path), type, null, name),
|
this.upload(key, fs.createReadStream(path), type, null, name),
|
||||||
];
|
];
|
||||||
|
@ -211,7 +212,7 @@ export class DriveService {
|
||||||
webpublicKey = `${prefix}webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
webpublicKey = `${prefix}webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||||
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
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));
|
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}`;
|
thumbnailKey = `${prefix}thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||||
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
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`));
|
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);
|
const [url, thumbnailUrl, webpublicUrl] = await Promise.all(promises);
|
||||||
|
|
||||||
if (thumbnailUrl) {
|
if (thumbnailUrl) {
|
||||||
this.registerLogger.info(`thumbnail stored: ${thumbnailAccessKey}`);
|
this.registerLogger.debug(`thumbnail stored: ${thumbnailAccessKey}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webpublicUrl) {
|
if (webpublicUrl) {
|
||||||
this.registerLogger.info(`web stored: ${webpublicAccessKey}`);
|
this.registerLogger.debug(`web stored: ${webpublicAccessKey}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
file.storedInternal = true;
|
file.storedInternal = true;
|
||||||
|
@ -311,7 +312,7 @@ export class DriveService {
|
||||||
thumbnail,
|
thumbnail,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.registerLogger.warn(`GenerateVideoThumbnail failed: ${err}`);
|
this.registerLogger.warn(`GenerateVideoThumbnail failed: ${renderInlineError(err)}`);
|
||||||
return {
|
return {
|
||||||
webpublic: null,
|
webpublic: null,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
|
@ -344,7 +345,7 @@ export class DriveService {
|
||||||
metadata.height && metadata.height <= 2048
|
metadata.height && metadata.height <= 2048
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.registerLogger.warn(`sharp failed: ${err}`);
|
this.registerLogger.warn(`sharp failed: ${renderInlineError(err)}`);
|
||||||
return {
|
return {
|
||||||
webpublic: null,
|
webpublic: null,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
|
@ -355,7 +356,7 @@ export class DriveService {
|
||||||
let webpublic: IImage | null = null;
|
let webpublic: IImage | null = null;
|
||||||
|
|
||||||
if (generateWeb && !satisfyWebpublic && !isAnimated) {
|
if (generateWeb && !satisfyWebpublic && !isAnimated) {
|
||||||
this.registerLogger.info('creating web image');
|
this.registerLogger.debug('creating web image');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
|
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);
|
this.registerLogger.warn('web image not created (an error occurred)', err as Error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (satisfyWebpublic) this.registerLogger.info('web image not created (original satisfies webpublic)');
|
if (satisfyWebpublic) this.registerLogger.debug('web image not created (original satisfies webpublic)');
|
||||||
else if (isAnimated) this.registerLogger.info('web image not created (animated image)');
|
else if (isAnimated) this.registerLogger.debug('web image not created (animated image)');
|
||||||
else this.registerLogger.info('web image not created (from remote)');
|
else this.registerLogger.debug('web image not created (from remote)');
|
||||||
}
|
}
|
||||||
// #endregion webpublic
|
// #endregion webpublic
|
||||||
|
|
||||||
|
@ -498,7 +499,6 @@ export class DriveService {
|
||||||
}: AddFileArgs): Promise<MiDriveFile> {
|
}: AddFileArgs): Promise<MiDriveFile> {
|
||||||
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
|
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
|
||||||
const info = await this.fileInfoService.getFileInfo(path);
|
const info = await this.fileInfoService.getFileInfo(path);
|
||||||
this.registerLogger.info(`${JSON.stringify(info)}`);
|
|
||||||
|
|
||||||
// detect name
|
// detect name
|
||||||
const detectedName = correctFilename(
|
const detectedName = correctFilename(
|
||||||
|
@ -508,6 +508,8 @@ export class DriveService {
|
||||||
ext ?? info.type.ext,
|
ext ?? info.type.ext,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.registerLogger.debug(`Detected file info: ${JSON.stringify(info)}`);
|
||||||
|
|
||||||
if (user && !force) {
|
if (user && !force) {
|
||||||
// Check if there is a file with the same hash
|
// Check if there is a file with the same hash
|
||||||
const matched = await this.driveFilesRepository.findOneBy({
|
const matched = await this.driveFilesRepository.findOneBy({
|
||||||
|
@ -516,7 +518,7 @@ export class DriveService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matched) {
|
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) {
|
if (sensitive && !matched.isSensitive) {
|
||||||
// The file is federated as sensitive for this time, but was federated as non-sensitive before.
|
// The file is federated as sensitive for this time, but was federated as non-sensitive before.
|
||||||
// Therefore, update the file to sensitive.
|
// Therefore, update the file to sensitive.
|
||||||
|
@ -644,14 +646,14 @@ export class DriveService {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// duplicate key error (when already registered)
|
// duplicate key error (when already registered)
|
||||||
if (isDuplicateKeyValueError(err)) {
|
if (isDuplicateKeyValueError(err)) {
|
||||||
this.registerLogger.info(`already registered ${file.uri}`);
|
this.registerLogger.debug(`already registered ${file.uri}`);
|
||||||
|
|
||||||
file = await this.driveFilesRepository.findOneBy({
|
file = await this.driveFilesRepository.findOneBy({
|
||||||
uri: file.uri!,
|
uri: file.uri!,
|
||||||
userId: user ? user.id : IsNull(),
|
userId: user ? user.id : IsNull(),
|
||||||
}) as MiDriveFile;
|
}) as MiDriveFile;
|
||||||
} else {
|
} else {
|
||||||
this.registerLogger.error(err as Error);
|
this.registerLogger.error('Error in drive register', err as Error);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -659,7 +661,7 @@ export class DriveService {
|
||||||
file = await (this.save(file, path, detectedName, info));
|
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 ?? '<none>'}`);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
|
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 });
|
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!;
|
return driveFile!;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.downloaderLogger.error(`Failed to create drive file: ${err}`, {
|
this.downloaderLogger.error(`Failed to create drive file from ${url}: ${renderInlineError(err)}`);
|
||||||
url: url,
|
|
||||||
e: err,
|
|
||||||
});
|
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import type { CheerioAPI } from 'cheerio';
|
import type { CheerioAPI } from 'cheerio';
|
||||||
|
|
||||||
type NodeInfo = {
|
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([
|
const [info, dom, manifest] = await Promise.all([
|
||||||
this.fetchNodeinfo(instance).catch(() => null),
|
this.fetchNodeinfo(instance).catch(() => null),
|
||||||
|
@ -106,7 +107,7 @@ export class FetchInstanceMetadataService {
|
||||||
this.getDescription(info, dom, manifest).catch(() => null),
|
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 = {
|
const updates = {
|
||||||
infoUpdatedAt: new Date(),
|
infoUpdatedAt: new Date(),
|
||||||
|
@ -128,9 +129,9 @@ export class FetchInstanceMetadataService {
|
||||||
|
|
||||||
await this.federatedInstanceService.update(instance.id, updates);
|
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) {
|
} 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 {
|
} finally {
|
||||||
await this.unlock(host);
|
await this.unlock(host);
|
||||||
}
|
}
|
||||||
|
@ -138,7 +139,7 @@ export class FetchInstanceMetadataService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchNodeinfo(instance: MiInstance): Promise<NodeInfo> {
|
private async fetchNodeinfo(instance: MiInstance): Promise<NodeInfo> {
|
||||||
this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
|
this.logger.debug(`Fetching nodeinfo of ${instance.host} ...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
||||||
|
@ -170,11 +171,11 @@ export class FetchInstanceMetadataService {
|
||||||
throw err.statusCode ?? err.message;
|
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;
|
return info as NodeInfo;
|
||||||
} catch (err) {
|
} 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;
|
throw err;
|
||||||
}
|
}
|
||||||
|
@ -182,7 +183,7 @@ export class FetchInstanceMetadataService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchDom(instance: MiInstance): Promise<CheerioAPI> {
|
private async fetchDom(instance: MiInstance): Promise<CheerioAPI> {
|
||||||
this.logger.info(`Fetching HTML of ${instance.host} ...`);
|
this.logger.debug(`Fetching HTML of ${instance.host} ...`);
|
||||||
|
|
||||||
const url = 'https://' + instance.host;
|
const url = 'https://' + instance.host;
|
||||||
|
|
||||||
|
|
|
@ -46,11 +46,13 @@ const TYPE_SVG = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileInfoService {
|
export class FileInfoService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private ffprobeLogger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('file-info');
|
this.logger = this.loggerService.getLogger('file-info');
|
||||||
|
this.ffprobeLogger = this.logger.createSubLogger('ffprobe');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -162,20 +164,19 @@ export class FileInfoService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private hasVideoTrackOnVideoFile(path: string): Promise<boolean> {
|
private hasVideoTrackOnVideoFile(path: string): Promise<boolean> {
|
||||||
const sublogger = this.logger.createSubLogger('ffprobe');
|
this.ffprobeLogger.debug(`Checking the video file. File path: ${path}`);
|
||||||
sublogger.info(`Checking the video file. File path: ${path}`);
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
FFmpeg.ffprobe(path, (err, metadata) => {
|
FFmpeg.ffprobe(path, (err, metadata) => {
|
||||||
if (err) {
|
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);
|
resolve(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve(metadata.streams.some((stream) => stream.codec_type === 'video'));
|
resolve(metadata.streams.some((stream) => stream.codec_type === 'video'));
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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);
|
resolve(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -331,7 +331,7 @@ export class HttpRequestService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok && extra.throwErrorWhenResponseNotOk) {
|
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) {
|
if (res.ok) {
|
||||||
|
|
|
@ -296,7 +296,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
case 'followers':
|
case 'followers':
|
||||||
// 他人のfollowers noteはreject
|
// 他人のfollowers noteはreject
|
||||||
if (data.renote.userId !== user.id) {
|
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にする
|
// Renote対象がfollowersならfollowersにする
|
||||||
|
@ -304,7 +304,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
break;
|
break;
|
||||||
case 'specified':
|
case 'specified':
|
||||||
// specified / direct noteはreject
|
// 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) {
|
if (data.renote.userId !== user.id) {
|
||||||
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
|
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
|
||||||
if (blocked) {
|
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
|
// should really not happen, but better safe than sorry
|
||||||
if (data.reply?.id === insert.id) {
|
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) {
|
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;
|
if (data.uri != null) insert.uri = data.uri;
|
||||||
|
@ -549,8 +549,6 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(e);
|
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,7 +309,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
|
|
||||||
if (this.isRenote(data)) {
|
if (this.isRenote(data)) {
|
||||||
if (data.renote.id === oldnote.id) {
|
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) {
|
switch (data.renote.visibility) {
|
||||||
|
@ -325,7 +325,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
case 'followers':
|
case 'followers':
|
||||||
// 他人のfollowers noteはreject
|
// 他人のfollowers noteはreject
|
||||||
if (data.renote.userId !== user.id) {
|
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にする
|
// Renote対象がfollowersならfollowersにする
|
||||||
|
@ -333,7 +333,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
break;
|
break;
|
||||||
case 'specified':
|
case 'specified':
|
||||||
// specified / direct noteはreject
|
// 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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ export class NotePiningService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (note == null) {
|
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 => {
|
await this.db.transaction(async tem => {
|
||||||
|
@ -102,7 +102,7 @@ export class NotePiningService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (note == null) {
|
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({
|
this.userNotePiningsRepository.delete({
|
||||||
|
|
|
@ -117,7 +117,7 @@ export class ReactionService {
|
||||||
if (note.userId !== user.id) {
|
if (note.userId !== user.id) {
|
||||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||||
if (blocked) {
|
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) {
|
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
|
// Delete reaction
|
||||||
const result = await this.noteReactionsRepository.delete(exist.id);
|
const result = await this.noteReactionsRepository.delete(exist.id);
|
||||||
|
|
||||||
if (result.affected !== 1) {
|
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
|
// Decrement reactions count
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { URL } from 'node:url';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
@ -18,6 +17,7 @@ import { RemoteLoggerService } from '@/core/RemoteLoggerService.js';
|
||||||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RemoteUserResolveService {
|
export class RemoteUserResolveService {
|
||||||
|
@ -44,27 +44,13 @@ export class RemoteUserResolveService {
|
||||||
const usernameLower = username.toLowerCase();
|
const usernameLower = username.toLowerCase();
|
||||||
|
|
||||||
if (host == null) {
|
if (host == null) {
|
||||||
this.logger.info(`return local user: ${usernameLower}`);
|
return await this.usersRepository.findOneByOrFail({ usernameLower, host: IsNull() }) as MiLocalUser;
|
||||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
|
||||||
if (u == null) {
|
|
||||||
throw new Error('user not found');
|
|
||||||
} else {
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
}) as MiLocalUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
if (host === this.utilityService.toPuny(this.config.host)) {
|
if (host === this.utilityService.toPuny(this.config.host)) {
|
||||||
this.logger.info(`return local user: ${usernameLower}`);
|
return await this.usersRepository.findOneByOrFail({ usernameLower, host: IsNull() }) as MiLocalUser;
|
||||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
|
||||||
if (u == null) {
|
|
||||||
throw new Error('user not found');
|
|
||||||
} else {
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
}) as MiLocalUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as MiRemoteUser | null;
|
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as MiRemoteUser | null;
|
||||||
|
@ -82,7 +68,7 @@ export class RemoteUserResolveService {
|
||||||
.getUserFromApId(self.href)
|
.getUserFromApId(self.href)
|
||||||
.then((u) => {
|
.then((u) => {
|
||||||
if (u == null) {
|
if (u == null) {
|
||||||
throw new Error('local user not found');
|
throw new Error(`local user not found: ${self.href}`);
|
||||||
} else {
|
} else {
|
||||||
return u;
|
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);
|
return await this.apPersonService.createPerson(self.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,18 +87,16 @@ export class RemoteUserResolveService {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.info(`try resync: ${acctLower}`);
|
|
||||||
const self = await this.resolveSelf(acctLower);
|
const self = await this.resolveSelf(acctLower);
|
||||||
|
|
||||||
if (user.uri !== self.href) {
|
if (user.uri !== self.href) {
|
||||||
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
|
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
|
||||||
this.logger.info(`uri missmatch: ${acctLower}`);
|
this.logger.warn(`Detected URI mismatch for ${acctLower}`);
|
||||||
this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
|
|
||||||
|
|
||||||
// validate uri
|
// validate uri
|
||||||
const uri = new URL(self.href);
|
const uriHost = this.utilityService.extractDbHost(self.href);
|
||||||
if (uri.hostname !== host) {
|
if (uriHost !== host) {
|
||||||
throw new Error('Invalid uri');
|
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({
|
await this.usersRepository.update({
|
||||||
|
@ -121,37 +105,28 @@ export class RemoteUserResolveService {
|
||||||
}, {
|
}, {
|
||||||
uri: self.href,
|
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);
|
await this.apPersonService.updatePerson(self.href);
|
||||||
|
|
||||||
this.logger.info(`return resynced remote user: ${acctLower}`);
|
return await this.usersRepository.findOneByOrFail({ uri: self.href }) as MiLocalUser | MiRemoteUser;
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`return existing remote user: ${acctLower}`);
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async resolveSelf(acctLower: string): Promise<ILink> {
|
private async resolveSelf(acctLower: string): Promise<ILink> {
|
||||||
this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
|
|
||||||
const finger = await this.webfingerService.webfinger(acctLower).catch(err => {
|
const finger = await this.webfingerService.webfinger(acctLower).catch(err => {
|
||||||
this.logger.error(`Failed to WebFinger for ${chalk.yellow(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}: ${ err.statusCode ?? err.message }`);
|
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');
|
const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self');
|
||||||
if (!self) {
|
if (!self) {
|
||||||
this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`);
|
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;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MiUser } from '@/models/_.js';
|
import { MiUser } from '@/models/_.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import Logger from '@/logger.js';
|
||||||
import type {
|
import type {
|
||||||
AuthenticationResponseJSON,
|
AuthenticationResponseJSON,
|
||||||
AuthenticatorTransportFuture,
|
AuthenticatorTransportFuture,
|
||||||
|
@ -28,6 +30,8 @@ import type {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebAuthnService {
|
export class WebAuthnService {
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
@ -40,7 +44,9 @@ export class WebAuthnService {
|
||||||
|
|
||||||
@Inject(DI.userSecurityKeysRepository)
|
@Inject(DI.userSecurityKeysRepository)
|
||||||
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||||
|
loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
|
this.logger = loggerService.getLogger('web-authn');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -114,8 +120,8 @@ export class WebAuthnService {
|
||||||
requireUserVerification: true,
|
requireUserVerification: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
this.logger.error(error as Error, 'Error authenticating webauthn');
|
||||||
throw new IdentifiableError('5c1446f8-8ca7-4d31-9f39-656afe9c5d87', 'verification failed');
|
throw new IdentifiableError('5c1446f8-8ca7-4d31-9f39-656afe9c5d87', 'verification failed', true, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { verified } = verification;
|
const { verified } = verification;
|
||||||
|
@ -221,7 +227,7 @@ export class WebAuthnService {
|
||||||
requireUserVerification: true,
|
requireUserVerification: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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;
|
const { verified, authenticationInfo } = verification;
|
||||||
|
@ -301,8 +307,8 @@ export class WebAuthnService {
|
||||||
requireUserVerification: true,
|
requireUserVerification: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
this.logger.error(error as Error, 'Error authenticating webauthn');
|
||||||
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed');
|
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed', true, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { verified, authenticationInfo } = verification;
|
const { verified, authenticationInfo } = verification;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { XMLParser } from 'fast-xml-parser';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { RemoteLoggerService } from './RemoteLoggerService.js';
|
import { RemoteLoggerService } from './RemoteLoggerService.js';
|
||||||
|
|
||||||
export type ILink = {
|
export type ILink = {
|
||||||
|
@ -109,7 +110,7 @@ export class WebfingerService {
|
||||||
const template = (hostMeta['XRD']['Link'] as Array<any>).filter(p => p['@_rel'] === 'lrdd')[0]['@_template'];
|
const template = (hostMeta['XRD']['Link'] as Array<any>).filter(p => p['@_rel'] === 'lrdd')[0]['@_template'];
|
||||||
return template.indexOf('{uri}') < 0 ? null : template;
|
return template.indexOf('{uri}') < 0 ? null : template;
|
||||||
} catch (err) {
|
} 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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,18 +165,23 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async refetchPublicKeyForApId(user: MiRemoteUser): Promise<MiUserPublickey | null> {
|
public async refetchPublicKeyForApId(user: MiRemoteUser): Promise<MiUserPublickey | null> {
|
||||||
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);
|
await this.apPersonService.updatePerson(user.uri);
|
||||||
|
const newKey = await this.apPersonService.findPublicKeyByUserId(user.id);
|
||||||
|
|
||||||
const key = await this.apPersonService.findPublicKeyByUserId(user.id);
|
if (newKey) {
|
||||||
|
if (oldKey && newKey.keyPem === oldKey.keyPem) {
|
||||||
if (key) {
|
this.apLoggerService.logger.debug(`Public key is up-to-date for user ${user.id} (${user.uri})`);
|
||||||
this.apLoggerService.logger.info('Re-fetched public key for user', { userId: user.id, uri: user.uri });
|
|
||||||
} else {
|
} else {
|
||||||
this.apLoggerService.logger.warn('Failed to re-fetch key for user', { userId: user.id, uri: user.uri });
|
this.apLoggerService.logger.info(`Updated public key for user ${user.id} (${user.uri})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.apLoggerService.logger.warn(`Failed to update public key for user ${user.id} (${user.uri})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return key;
|
return newKey ?? oldKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -57,7 +57,7 @@ class DeliverManager {
|
||||||
) {
|
) {
|
||||||
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// 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のみに絞る
|
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
||||||
this.actor = {
|
this.actor = {
|
||||||
|
@ -124,12 +124,13 @@ class DeliverManager {
|
||||||
select: {
|
select: {
|
||||||
followerSharedInbox: true,
|
followerSharedInbox: true,
|
||||||
followerInbox: true,
|
followerInbox: true,
|
||||||
|
followerId: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const following of followers) {
|
for (const following of followers) {
|
||||||
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
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);
|
inboxes.set(inbox, following.followerSharedInbox != null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { fromTuple } from '@/misc/from-tuple.js';
|
import { fromTuple } from '@/misc/from-tuple.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.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 InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import FederationChart from '@/core/chart/charts/federation.js';
|
import FederationChart from '@/core/chart/charts/federation.js';
|
||||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||||
|
@ -121,13 +122,14 @@ export class ApInboxService {
|
||||||
act.id = undefined;
|
act.id = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const id = getNullableApId(act) ?? `${getNullableApId(activity)}#${i}`;
|
const id = getNullableApId(act) ?? `${getNullableApId(activity)}#${i}`;
|
||||||
|
|
||||||
|
try {
|
||||||
const result = await this.performOneActivity(actor, act, resolver);
|
const result = await this.performOneActivity(actor, act, resolver);
|
||||||
results.push([id, result]);
|
results.push([id, result]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error || typeof err === 'string') {
|
if (err instanceof Error || typeof err === 'string') {
|
||||||
this.logger.error(err);
|
this.logger.error(`Unhandled error in activity ${id}:`, err);
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
@ -147,7 +149,8 @@ export class ApInboxService {
|
||||||
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
// 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない
|
// 同一ユーザーの情報を再度処理するので、使用済みの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();
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(err => {
|
const object = await resolver.resolve(activity.object).catch(err => {
|
||||||
this.logger.error(`Resolution failed: ${err}`);
|
this.logger.error(`Resolution failed: ${renderInlineError(err)}`);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -326,7 +329,7 @@ export class ApInboxService {
|
||||||
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
|
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
|
||||||
|
|
||||||
const target = await resolver.secureResolve(activityObject, uri).catch(e => {
|
const target = await resolver.secureResolve(activityObject, uri).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${renderInlineError(e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -357,22 +360,10 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Announce対象をresolve
|
// 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.
|
// 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.
|
// 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) });
|
const renote = await this.apNoteService.resolveNote(target, { resolver, sentFrom: getApId(target) });
|
||||||
if (renote == null) return 'announce target is null';
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
|
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
|
||||||
return 'skip: invalid actor for this activity';
|
return 'skip: invalid actor for this activity';
|
||||||
|
@ -454,9 +445,11 @@ export class ApInboxService {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
// Don't re-use the resolver, or it may throw recursion errors.
|
// Don't re-use the resolver, or it may throw recursion errors.
|
||||||
// Instead, create a new resolver with an appropriately-reduced recursion limit.
|
// 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,
|
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();
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activityObject).catch(e => {
|
const object = await resolver.resolve(activityObject).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${renderInlineError(e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -548,12 +541,6 @@ export class ApInboxService {
|
||||||
|
|
||||||
await this.apNoteService.createNote(note, actor, resolver, silent);
|
await this.apNoteService.createNote(note, actor, resolver, silent);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof StatusError && !err.isRetryable) {
|
|
||||||
return `skip: ${err.statusCode}`;
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
unlock();
|
unlock();
|
||||||
}
|
}
|
||||||
|
@ -686,7 +673,7 @@ export class ApInboxService {
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${renderInlineError(e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -758,7 +745,7 @@ export class ApInboxService {
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${renderInlineError(e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -890,7 +877,7 @@ export class ApInboxService {
|
||||||
resolver ??= this.apResolverService.createResolver();
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${renderInlineError(e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ export class Resolver {
|
||||||
if (isCollectionOrOrderedCollection(collection)) {
|
if (isCollectionOrOrderedCollection(collection)) {
|
||||||
return collection;
|
return collection;
|
||||||
} else {
|
} 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.
|
// 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.
|
// 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.
|
// 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
|
// URLs with fragment parts cannot be resolved correctly because
|
||||||
// the fragment part does not get transmitted over HTTP(S).
|
// the fragment part does not get transmitted over HTTP(S).
|
||||||
// Avoid strange behaviour by not trying to resolve these at all.
|
// 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)) {
|
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) {
|
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);
|
this.history.add(value);
|
||||||
|
@ -294,7 +294,7 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.utilityService.isFederationAllowedHost(host)) {
|
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) {
|
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'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
||||||
object['@context'] !== '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.
|
// 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.
|
// 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.
|
// 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.
|
// 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].
|
// Check if the redirect bounce from [allowed domain] to [blocked domain].
|
||||||
if (!this.utilityService.isFederationAllowedHost(finalHost)) {
|
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
|
@bindThis
|
||||||
private resolveLocal(url: string): Promise<IObjectWithId> {
|
private resolveLocal(url: string): Promise<IObjectWithId> {
|
||||||
const parsed = this.apDbResolverService.parseUri(url);
|
const parsed = this.apDbResolverService.parseUri(url);
|
||||||
if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `resolveLocal - not a local URL: ${url}`);
|
if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `failed to resolve local ${url}: not a local URL`);
|
||||||
|
|
||||||
switch (parsed.type) {
|
switch (parsed.type) {
|
||||||
case 'notes':
|
case 'notes':
|
||||||
|
@ -385,7 +385,7 @@ export class Resolver {
|
||||||
case 'follows':
|
case 'follows':
|
||||||
return this.followRequestsRepository.findOneBy({ id: parsed.id })
|
return this.followRequestsRepository.findOneBy({ id: parsed.id })
|
||||||
.then(async followRequest => {
|
.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([
|
const [follower, followee] = await Promise.all([
|
||||||
this.usersRepository.findOneBy({
|
this.usersRepository.findOneBy({
|
||||||
id: followRequest.followerId,
|
id: followRequest.followerId,
|
||||||
|
@ -397,12 +397,12 @@ export class Resolver {
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
if (follower == null || followee == null) {
|
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));
|
return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
|
||||||
});
|
});
|
||||||
default:
|
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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ export class ApUtilityService {
|
||||||
public assertIdMatchesUrlAuthority(object: IObject, url: string): void {
|
public assertIdMatchesUrlAuthority(object: IObject, url: string): void {
|
||||||
// This throws if the ID is missing or invalid, but that's ok.
|
// 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.
|
// 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).
|
// 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.
|
// The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { UnrecoverableError } from 'bullmq';
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
|
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
|
||||||
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
|
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
|
||||||
import type { JsonLdDocument } from 'jsonld';
|
import type { JsonLdDocument } from 'jsonld';
|
||||||
|
@ -149,7 +150,7 @@ class JsonLd {
|
||||||
},
|
},
|
||||||
).then(res => {
|
).then(res => {
|
||||||
if (!res.ok) {
|
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 {
|
} else {
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type { Response } from 'node-fetch';
|
import type { Response } from 'node-fetch';
|
||||||
|
|
||||||
// TODO throw identifiable or unrecoverable errors
|
|
||||||
|
|
||||||
export function validateContentTypeSetAsActivityPub(response: Response): void {
|
export function validateContentTypeSetAsActivityPub(response: Response): void {
|
||||||
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
||||||
|
|
||||||
if (contentType === '') {
|
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 (
|
if (
|
||||||
contentType.startsWith('application/activity+json') ||
|
contentType.startsWith('application/activity+json') ||
|
||||||
|
@ -19,7 +18,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void {
|
||||||
) {
|
) {
|
||||||
return;
|
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*(;|$)/;
|
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();
|
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
||||||
|
|
||||||
if (contentType === '') {
|
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 (
|
if (
|
||||||
contentType.startsWith('application/ld+json') ||
|
contentType.startsWith('application/ld+json') ||
|
||||||
|
@ -37,5 +36,5 @@ export function validateContentTypeSetAsJsonLD(response: Response): void {
|
||||||
) {
|
) {
|
||||||
return;
|
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`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import type { Config } from '@/config.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { isDocument, type IObject } from '../type.js';
|
import { getNullableApId, isDocument, type IObject } from '../type.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApImageService {
|
export class ApImageService {
|
||||||
|
@ -48,7 +48,7 @@ export class ApImageService {
|
||||||
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
|
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
|
||||||
// 投稿者が凍結されていたらスキップ
|
// 投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
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);
|
const image = await this.apResolverService.createResolver().resolve(value);
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { isRetryableError } from '@/misc/is-retryable-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 { getOneApId, getApId, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApMfmService } from '../ApMfmService.js';
|
import { ApMfmService } from '../ApMfmService.js';
|
||||||
|
@ -100,29 +101,29 @@ export class ApNoteService {
|
||||||
const apType = getApType(object);
|
const apType = getApType(object);
|
||||||
|
|
||||||
if (apType == null || !validPost.includes(apType)) {
|
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) {
|
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));
|
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
|
||||||
if (object.attributedTo && actualHost !== expectHost) {
|
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())) {
|
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) {
|
if (actor) {
|
||||||
const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri;
|
const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri;
|
||||||
if (attribution !== 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) {
|
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 entryUri = getApId(value);
|
||||||
const err = this.validateNote(object, entryUri, actor);
|
const err = this.validateNote(object, entryUri, actor);
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err.message, {
|
this.logger.error(`Error creating note: ${renderInlineError(err)}`, {
|
||||||
resolver: { history: resolver.getHistory() },
|
resolver: { history: resolver.getHistory() },
|
||||||
value,
|
value,
|
||||||
object,
|
object,
|
||||||
|
@ -174,11 +175,11 @@ export class ApNoteService {
|
||||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||||
|
|
||||||
if (note.id == null) {
|
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)) {
|
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);
|
const url = this.apUtilityService.findBestObjectUrl(note);
|
||||||
|
@ -187,7 +188,7 @@ export class ApNoteService {
|
||||||
|
|
||||||
// 投稿者をフェッチ
|
// 投稿者をフェッチ
|
||||||
if (note.attributedTo == null) {
|
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);
|
const uri = getOneApId(note.attributedTo);
|
||||||
|
@ -196,7 +197,7 @@ export class ApNoteService {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined;
|
actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined;
|
||||||
if (actor && actor.isSuspended) {
|
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);
|
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 });
|
const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||||
if (hasProhibitedWords) {
|
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
|
//#endregion
|
||||||
|
|
||||||
|
@ -232,7 +233,7 @@ export class ApNoteService {
|
||||||
|
|
||||||
// 解決した投稿者が凍結されていたらスキップ
|
// 解決した投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
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);
|
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 })
|
? await this.resolveNote(note.inReplyTo, { resolver })
|
||||||
.then(x => {
|
.then(x => {
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
this.logger.warn('Specified inReplyTo, but not found');
|
this.logger.warn(`Specified inReplyTo "${note.inReplyTo}", but not found`);
|
||||||
throw new Error(`could not fetch 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
})
|
})
|
||||||
.catch(async err => {
|
.catch(async err => {
|
||||||
this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
this.logger.warn(`error ${renderInlineError(err)} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
||||||
throw err;
|
throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to create note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true, err);
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
@ -348,7 +349,7 @@ export class ApNoteService {
|
||||||
this.logger.info('The note is already inserted while creating itself, reading again');
|
this.logger.info('The note is already inserted while creating itself, reading again');
|
||||||
const duplicate = await this.fetchNote(value);
|
const duplicate = await this.fetchNote(value);
|
||||||
if (!duplicate) {
|
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;
|
return duplicate;
|
||||||
}
|
}
|
||||||
|
@ -362,45 +363,39 @@ export class ApNoteService {
|
||||||
const noteUri = getApId(value);
|
const noteUri = getApId(value);
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// 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 このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri });
|
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;
|
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
|
resolver ??= this.apResolverService.createResolver();
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
|
||||||
|
|
||||||
const object = await resolver.resolve(value);
|
const object = await resolver.resolve(value);
|
||||||
|
|
||||||
const entryUri = getApId(value);
|
const entryUri = getApId(value);
|
||||||
const err = this.validateNote(object, entryUri, actor, user);
|
const err = this.validateNote(object, entryUri, actor, user);
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err.message, {
|
this.logger.error(`Failed to update note ${noteUri}: ${renderInlineError(err)}`);
|
||||||
resolver: { history: resolver.getHistory() },
|
|
||||||
value,
|
|
||||||
object,
|
|
||||||
});
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `validateNote` checks that the actor and user are one and the same
|
// `validateNote` checks that the actor and user are one and the same
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
actor ??= user;
|
actor ??= user;
|
||||||
|
|
||||||
const note = object as IPost;
|
const note = object as IPost;
|
||||||
|
|
||||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
|
||||||
|
|
||||||
if (note.id == null) {
|
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)) {
|
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);
|
const url = this.apUtilityService.findBestObjectUrl(note);
|
||||||
|
@ -408,7 +403,7 @@ export class ApNoteService {
|
||||||
this.logger.info(`Creating the Note: ${note.id}`);
|
this.logger.info(`Creating the Note: ${note.id}`);
|
||||||
|
|
||||||
if (actor.isSuspended) {
|
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);
|
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 });
|
const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||||
if (hasProhibitedWords) {
|
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
|
//#endregion
|
||||||
|
|
||||||
|
@ -473,15 +468,15 @@ export class ApNoteService {
|
||||||
? await this.resolveNote(note.inReplyTo, { resolver })
|
? await this.resolveNote(note.inReplyTo, { resolver })
|
||||||
.then(x => {
|
.then(x => {
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
this.logger.warn('Specified inReplyTo, but not found');
|
this.logger.warn(`Specified inReplyTo "${note.inReplyTo}", but not found`);
|
||||||
throw new Error(`could not fetch 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
})
|
})
|
||||||
.catch(async err => {
|
.catch(async err => {
|
||||||
this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
this.logger.warn(`error ${renderInlineError(err)} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
||||||
throw err;
|
throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to update note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true, err);
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
@ -549,7 +544,7 @@ export class ApNoteService {
|
||||||
this.logger.info('The note is already inserted while creating itself, reading again');
|
this.logger.info('The note is already inserted while creating itself, reading again');
|
||||||
const duplicate = await this.fetchNote(value);
|
const duplicate = await this.fetchNote(value);
|
||||||
if (!duplicate) {
|
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;
|
return duplicate;
|
||||||
}
|
}
|
||||||
|
@ -566,8 +561,7 @@ export class ApNoteService {
|
||||||
const uri = getApId(value);
|
const uri = getApId(value);
|
||||||
|
|
||||||
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
||||||
// TODO convert to identifiable error
|
throw new IdentifiableError('04620a7e-044e-45ce-b72c-10e1bdc22e69', `failed to resolve note ${uri}: host is blocked`);
|
||||||
throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
|
@ -577,8 +571,7 @@ export class ApNoteService {
|
||||||
|
|
||||||
// Bail if local URI doesn't exist
|
// Bail if local URI doesn't exist
|
||||||
if (this.utilityService.isUriLocal(uri)) {
|
if (this.utilityService.isUriLocal(uri)) {
|
||||||
// TODO convert to identifiable error
|
throw new IdentifiableError('cbac7358-23f2-4c70-833e-cffb4bf77913', `failed to resolve note ${uri}: URL is local and does not exist`);
|
||||||
throw new StatusError(`cannot resolve local note: ${uri}`, 400, 'cannot resolve local note');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
@ -685,18 +678,13 @@ export class ApNoteService {
|
||||||
const quote = await this.resolveNote(uri, { resolver });
|
const quote = await this.resolveNote(uri, { resolver });
|
||||||
|
|
||||||
if (quote == null) {
|
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 false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return quote;
|
return quote;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": ${renderInlineError(e)}`);
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isRetryableError(e);
|
return isRetryableError(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import promiseLimit from 'promise-limit';
|
import promiseLimit from 'promise-limit';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { AbortError } from 'node-fetch';
|
|
||||||
import { UnrecoverableError } from 'bullmq';
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.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 { MemoryKVCache } from '@/misc/cache.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { verifyFieldLinks } from '@/misc/verify-field-link.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 { getApId, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||||
import { extractApHashtags } from './tag.js';
|
import { extractApHashtags } from './tag.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
|
@ -54,6 +55,7 @@ import type { ApLoggerService } from '../ApLoggerService.js';
|
||||||
|
|
||||||
import type { ApImageService } from './ApImageService.js';
|
import type { ApImageService } from './ApImageService.js';
|
||||||
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
|
||||||
const nameLength = 128;
|
const nameLength = 128;
|
||||||
const summaryLength = 2048;
|
const summaryLength = 2048;
|
||||||
|
@ -157,21 +159,21 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
const expectHost = this.utilityService.punyHostPSLDomain(uri);
|
const expectHost = this.utilityService.punyHostPSLDomain(uri);
|
||||||
|
|
||||||
if (!isActor(x)) {
|
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)) {
|
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)) {
|
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);
|
this.apUtilityService.assertApUrl(x.inbox);
|
||||||
const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox);
|
const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox);
|
||||||
if (inboxHost !== expectHost) {
|
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);
|
const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined);
|
||||||
|
@ -179,7 +181,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
const sharedInbox = getApId(sharedInboxObject);
|
const sharedInbox = getApId(sharedInboxObject);
|
||||||
this.apUtilityService.assertApUrl(sharedInbox);
|
this.apUtilityService.assertApUrl(sharedInbox);
|
||||||
if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHostPSLDomain(sharedInbox) === expectHost)) {
|
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) {
|
if (typeof collectionUri === 'string' && collectionUri.length > 0) {
|
||||||
this.apUtilityService.assertApUrl(collectionUri);
|
this.apUtilityService.assertApUrl(collectionUri);
|
||||||
if (this.utilityService.punyHostPSLDomain(collectionUri) !== expectHost) {
|
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) {
|
} else if (collectionUri != null) {
|
||||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} type`);
|
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))) {
|
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
|
// 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.
|
// we can at least see these users and their activities.
|
||||||
if (x.name) {
|
if (x.name) {
|
||||||
if (!(typeof x.name === 'string' && x.name.length > 0)) {
|
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);
|
x.name = truncate(x.name, nameLength);
|
||||||
} else if (x.name === '') {
|
} else if (x.name === '') {
|
||||||
|
@ -216,24 +218,24 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
if (x.summary) {
|
if (x.summary) {
|
||||||
if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
|
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);
|
x.summary = truncate(x.summary, summaryLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
const idHost = this.utilityService.punyHostPSLDomain(x.id);
|
const idHost = this.utilityService.punyHostPSLDomain(x.id);
|
||||||
if (idHost !== expectHost) {
|
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 (x.publicKey) {
|
||||||
if (typeof x.publicKey.id !== 'string') {
|
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);
|
const publicKeyIdHost = this.utilityService.punyHostPSLDomain(x.publicKey.id);
|
||||||
if (publicKeyIdHost !== expectHost) {
|
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<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>>> {
|
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any, bgimg: any): Promise<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>>> {
|
||||||
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 => {
|
const [avatar, banner, background] = await Promise.all([icon, image, bgimg].map(img => {
|
||||||
// icon and image may be arrays
|
// icon and image may be arrays
|
||||||
// see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
|
// see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
|
||||||
|
@ -325,12 +325,11 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
|
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
|
||||||
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);
|
const host = this.utilityService.punyHost(uri);
|
||||||
if (host === this.utilityService.toPuny(this.config.host)) {
|
if (host === this.utilityService.toPuny(this.config.host)) {
|
||||||
// TODO convert to unrecoverable error
|
throw new UnrecoverableError(`failed to create user ${uri}: URI is local`);
|
||||||
throw new StatusError(`cannot resolve local user: ${uri}`, 400, 'cannot resolve local user');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this._createPerson(uri, resolver);
|
return await this._createPerson(uri, resolver);
|
||||||
|
@ -340,8 +339,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
const uri = getApId(value);
|
const uri = getApId(value);
|
||||||
const host = this.utilityService.punyHost(uri);
|
const host = this.utilityService.punyHost(uri);
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
resolver ??= this.apResolverService.createResolver();
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
|
||||||
|
|
||||||
const object = await resolver.resolve(value);
|
const object = await resolver.resolve(value);
|
||||||
const person = this.validateActor(object, uri);
|
const person = this.validateActor(object, uri);
|
||||||
|
@ -361,9 +359,11 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
].map((p): Promise<'public' | 'private'> => p
|
].map((p): Promise<'public' | 'private'> => p
|
||||||
.then(isPublic => isPublic ? 'public' : 'private')
|
.then(isPublic => isPublic ? 'public' : 'private')
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
// Permanent error implies hidden or inaccessible, which is a normal thing.
|
||||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
if (isRetryableError(err)) {
|
||||||
|
this.logger.error(`error occurred while fetching following/followers collection: ${renderInlineError(err)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'private';
|
return 'private';
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -372,7 +372,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
|
||||||
if (person.id == null) {
|
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);
|
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)
|
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host)
|
||||||
.then(_emojis => _emojis.map(emoji => emoji.name))
|
.then(_emojis => _emojis.map(emoji => emoji.name))
|
||||||
.catch(err => {
|
.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 [];
|
return [];
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -493,7 +496,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
user = u as MiRemoteUser;
|
user = u as MiRemoteUser;
|
||||||
publicKey = await this.userPublickeysRepository.findOneBy({ userId: user.id });
|
publicKey = await this.userPublickeysRepository.findOneBy({ userId: user.id });
|
||||||
} else {
|
} 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;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -533,11 +536,19 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
// Register to the cache
|
// Register to the cache
|
||||||
this.cacheService.uriPersonCache.set(user.uri, user);
|
this.cacheService.uriPersonCache.set(user.uri, user);
|
||||||
} catch (err) {
|
} 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
|
//#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;
|
return user;
|
||||||
}
|
}
|
||||||
|
@ -554,7 +565,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
|
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
|
||||||
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がこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (this.utilityService.isUriLocal(uri)) return;
|
if (this.utilityService.isUriLocal(uri)) return;
|
||||||
|
@ -574,8 +585,11 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
this.logger.info(`Updating the Person: ${person.id}`);
|
this.logger.info(`Updating the Person: ${person.id}`);
|
||||||
|
|
||||||
// カスタム絵文字取得
|
// カスタム絵文字取得
|
||||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
|
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(err => {
|
||||||
this.logger.info(`extractEmojis: ${e}`);
|
// 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 [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -592,11 +606,13 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
].map((p): Promise<'public' | 'private' | undefined> => p
|
].map((p): Promise<'public' | 'private' | undefined> => p
|
||||||
.then(isPublic => isPublic ? 'public' : 'private')
|
.then(isPublic => isPublic ? 'public' : 'private')
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
// Permanent error implies hidden or inaccessible, which is a normal thing.
|
||||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
if (isRetryableError(err)) {
|
||||||
|
this.logger.error(`error occurred while fetching following/followers collection: ${renderInlineError(err)}`);
|
||||||
// Do not update the visibility on transient errors.
|
// Do not update the visibility on transient errors.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'private';
|
return 'private';
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -605,7 +621,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
|
||||||
if (person.id == null) {
|
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);
|
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)
|
.filter((a: unknown) => typeof(a) === 'string' && a.length > 0 && a.length <= 128)
|
||||||
.slice(0, 32)
|
.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<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'speakAsCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'speakAsCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
||||||
|
|
||||||
const moving = ((): boolean => {
|
const moving = ((): boolean => {
|
||||||
|
@ -722,7 +746,12 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
{ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null },
|
{ 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 };
|
const updated = { ...exist, ...updates };
|
||||||
|
|
||||||
|
@ -761,8 +790,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
const uri = getApId(value);
|
const uri = getApId(value);
|
||||||
|
|
||||||
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
||||||
// TODO convert to identifiable error
|
throw new IdentifiableError('590719b3-f51f-48a9-8e7d-6f559ad00e5d', `failed to resolve person ${uri}: host is blocked`);
|
||||||
throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
|
@ -772,8 +800,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
|
|
||||||
// Bail if local URI doesn't exist
|
// Bail if local URI doesn't exist
|
||||||
if (this.utilityService.isUriLocal(uri)) {
|
if (this.utilityService.isUriLocal(uri)) {
|
||||||
// TODO convert to identifiable error
|
throw new IdentifiableError('efb573fd-6b9e-4912-9348-a02f5603df4f', `failed to resolve person ${uri}: URL is local and does not exist`);
|
||||||
throw new StatusError(`cannot resolve local person: ${uri}`, 400, 'cannot resolve local person');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
@ -818,15 +845,16 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
||||||
|
|
||||||
// Resolve to (Ordered)Collection Object
|
// Resolve to (Ordered)Collection Object
|
||||||
const collection = user.featured ? await _resolver.resolveCollection(user.featured, true, user.uri).catch(err => {
|
const collection = user.featured ? await _resolver.resolveCollection(user.featured, true, user.uri).catch(err => {
|
||||||
if (err instanceof AbortError || err instanceof StatusError) {
|
// Permanent error implies hidden or inaccessible, which is a normal thing.
|
||||||
this.logger.warn(`Failed to update featured notes: ${err.name}: ${err.message}`);
|
if (isRetryableError(err)) {
|
||||||
} else {
|
this.logger.warn(`Failed to update featured notes: ${renderInlineError(err)}`);
|
||||||
this.logger.error('Failed to update featured notes:', err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}) : null;
|
}) : null;
|
||||||
if (!collection) return;
|
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
|
// Resolve to Object(may be Note) arrays
|
||||||
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
||||||
|
|
|
@ -93,7 +93,6 @@ export class ApQuestionService {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
const question = await resolver.resolve(value);
|
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}`);
|
if (!isQuestion(question)) throw new UnrecoverableError(`object ${getApType(question)} is not a Question: ${uri}`);
|
||||||
|
|
||||||
|
|
|
@ -75,14 +75,17 @@ export function getOneApId(value: ApObject): string {
|
||||||
/**
|
/**
|
||||||
* Get ActivityStreams Object id
|
* Get ActivityStreams Object id
|
||||||
*/
|
*/
|
||||||
export function getApId(source: string | IObject | [string | IObject]): string {
|
export function getApId(value: string | IObject | [string | IObject], sourceForLogs?: string): string {
|
||||||
const value = getNullableApId(source);
|
const id = getNullableApId(value);
|
||||||
|
|
||||||
if (value == null) {
|
if (id == null) {
|
||||||
throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', `invalid AP object ${value}: missing or invalid id`);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,7 +21,7 @@ export class FileWriterStream extends WritableStream<Uint8Array> {
|
||||||
write: async (chunk, controller) => {
|
write: async (chunk, controller) => {
|
||||||
if (file === null) {
|
if (file === null) {
|
||||||
controller.error();
|
controller.error();
|
||||||
throw new Error();
|
throw new Error('file is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
await file.write(chunk);
|
await file.write(chunk);
|
||||||
|
|
|
@ -8,8 +8,8 @@ export class FastifyReplyError extends Error {
|
||||||
public message: string;
|
public message: string;
|
||||||
public statusCode: number;
|
public statusCode: number;
|
||||||
|
|
||||||
constructor(statusCode: number, message: string) {
|
constructor(statusCode: number, message: string, cause?: unknown) {
|
||||||
super(message);
|
super(message, cause ? { cause } : undefined);
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.statusCode = statusCode;
|
this.statusCode = statusCode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||||
|
import { IdentifiableError } from '../identifiable-error.js';
|
||||||
|
|
||||||
export const aidRegExp = /^[0-9a-z]{10}$/;
|
export const aidRegExp = /^[0-9a-z]{10}$/;
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ function getNoise(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function genAid(t: number): 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++;
|
counter++;
|
||||||
return getTime(t) + getNoise();
|
return getTime(t) + getNoise();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||||
|
import { IdentifiableError } from '../identifiable-error.js';
|
||||||
|
|
||||||
export const aidxRegExp = /^[0-9a-z]{16}$/;
|
export const aidxRegExp = /^[0-9a-z]{16}$/;
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ function getNoise(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function genAidx(t: number): 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++;
|
counter++;
|
||||||
return getTime(t) + nodeId + getNoise();
|
return getTime(t) + nodeId + getNoise();
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,8 @@ export class IdentifiableError extends Error {
|
||||||
*/
|
*/
|
||||||
public readonly isRetryable: boolean;
|
public readonly isRetryable: boolean;
|
||||||
|
|
||||||
constructor(id: string, message?: string, isRetryable = false) {
|
constructor(id: string, message?: string, isRetryable = false, cause?: unknown) {
|
||||||
super(message);
|
super(message, cause ? { cause } : undefined);
|
||||||
this.message = message ?? '';
|
this.message = message ?? '';
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.isRetryable = isRetryable;
|
this.isRetryable = isRetryable;
|
||||||
|
|
|
@ -3,20 +3,34 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AbortError } from 'node-fetch';
|
import { AbortError, FetchError } from 'node-fetch';
|
||||||
import { UnrecoverableError } from 'bullmq';
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-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 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.
|
* 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 {
|
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 StatusError) return e.isRetryable;
|
||||||
if (e instanceof IdentifiableError) 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 UnrecoverableError) return false;
|
||||||
if (e instanceof AbortError) return true;
|
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';
|
if (e instanceof Error) return e.name === 'AbortError';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
60
packages/backend/src/misc/render-full-error.ts
Normal file
60
packages/backend/src/misc/render-full-error.ts
Normal file
|
@ -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'][];
|
||||||
|
}
|
75
packages/backend/src/misc/render-inline-error.ts
Normal file
75
packages/backend/src/misc/render-inline-error.ts
Normal file
|
@ -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);
|
||||||
|
}
|
|
@ -9,8 +9,8 @@ export class StatusError extends Error {
|
||||||
public isClientError: boolean;
|
public isClientError: boolean;
|
||||||
public isRetryable: boolean;
|
public isRetryable: boolean;
|
||||||
|
|
||||||
constructor(message: string, statusCode: number, statusMessage?: string) {
|
constructor(message: string, statusCode: number, statusMessage?: string, cause?: unknown) {
|
||||||
super(message);
|
super(message, cause ? { cause } : undefined);
|
||||||
this.name = 'StatusError';
|
this.name = 'StatusError';
|
||||||
this.statusCode = statusCode;
|
this.statusCode = statusCode;
|
||||||
this.statusMessage = statusMessage;
|
this.statusMessage = statusMessage;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.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 { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.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 currentAttempts = job.attemptsMade + (increment ? 1 : 0);
|
||||||
const maxAttempts = job.opts.attempts ?? 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()
|
@Injectable()
|
||||||
|
@ -134,35 +136,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger;
|
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<string, string> = {
|
|
||||||
info: getJobInfo(job),
|
|
||||||
data: job.data,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (job.name) info.name = job.name;
|
|
||||||
if (job.failedReason) info.failedReason = job.failedReason;
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#region system
|
//#region system
|
||||||
{
|
{
|
||||||
const processer = (job: Bull.Job) => {
|
const processer = (job: Bull.Job) => {
|
||||||
|
@ -196,7 +169,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err: Error) => {
|
.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) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||||
level: 'error',
|
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}`));
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -261,7 +234,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err) => {
|
.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) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||||
level: 'error',
|
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}`));
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -301,7 +274,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
.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('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
.on('failed', (job, err) => {
|
.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) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
|
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
|
||||||
level: 'error',
|
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}`));
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -341,7 +314,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
|
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
|
||||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||||
.on('failed', (job, err) => {
|
.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) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
|
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
|
||||||
level: 'error',
|
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}`));
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -381,7 +354,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
.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('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
.on('failed', (job, err) => {
|
.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) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
|
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||||
level: 'error',
|
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}`));
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -421,7 +394,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
.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('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
.on('failed', (job, err) => {
|
.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) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
|
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||||
level: 'error',
|
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}`));
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -468,7 +441,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err) => {
|
.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) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||||
level: 'error',
|
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}`));
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -509,7 +482,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||||
.on('failed', (job, err) => {
|
.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) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||||
level: 'error',
|
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}`));
|
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region ended poll notification
|
//#region ended poll notification
|
||||||
{
|
{
|
||||||
|
const logger = this.logger.createSubLogger('endedPollNotification');
|
||||||
|
|
||||||
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
|
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
|
||||||
if (this.config.sentryForBackend) {
|
if (this.config.sentryForBackend) {
|
||||||
return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
|
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),
|
...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
||||||
autorun: false,
|
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
|
//#endregion
|
||||||
|
|
||||||
//#region schedule note post
|
//#region schedule note post
|
||||||
{
|
{
|
||||||
|
const logger = this.logger.createSubLogger('scheduleNotePost');
|
||||||
|
|
||||||
this.schedulerNotePostQueueWorker = new Bull.Worker(QUEUE.SCHEDULE_NOTE_POST, (job) => this.scheduleNotePostProcessorService.process(job), {
|
this.schedulerNotePostQueueWorker = new Bull.Worker(QUEUE.SCHEDULE_NOTE_POST, (job) => this.scheduleNotePostProcessorService.process(job), {
|
||||||
...baseWorkerOptions(this.config, QUEUE.SCHEDULE_NOTE_POST),
|
...baseWorkerOptions(this.config, QUEUE.SCHEDULE_NOTE_POST),
|
||||||
autorun: false,
|
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
|
//#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
|
@bindThis
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|
|
@ -62,7 +62,7 @@ export class AggregateRetentionProcessorService {
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isDuplicateKeyValueError(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;
|
return;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -87,6 +87,6 @@ export class AggregateRetentionProcessorService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('Retention aggregated.');
|
this.logger.info('Retention aggregated.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,6 @@ export class BakeBufferedReactionsProcessorService {
|
||||||
|
|
||||||
await this.reactionsBufferingService.bake();
|
await this.reactionsBufferingService.bake();
|
||||||
|
|
||||||
this.logger.succ('All buffered reactions baked.');
|
this.logger.info('All buffered reactions baked.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,6 @@ export class CheckExpiredMutingsProcessorService {
|
||||||
await this.userMutingService.unmute(expired);
|
await this.userMutingService.unmute(expired);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('All expired mutings checked.');
|
this.logger.info('All expired mutings checked.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,16 +98,16 @@ export class CheckModeratorsActivityProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(): Promise<void> {
|
public async process(): Promise<void> {
|
||||||
this.logger.info('start.');
|
this.logger.debug('start.');
|
||||||
|
|
||||||
const meta = await this.metaService.fetch(false);
|
const meta = await this.metaService.fetch(false);
|
||||||
if (!meta.disableRegistration) {
|
if (!meta.disableRegistration) {
|
||||||
await this.processImpl();
|
await this.processImpl();
|
||||||
} else {
|
} else {
|
||||||
this.logger.info('is already invitation only.');
|
this.logger.debug('is already invitation only.');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('finish.');
|
this.logger.debug('finish.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -62,6 +62,6 @@ export class CleanChartsProcessorService {
|
||||||
await this.perUserDriveChart.clean();
|
await this.perUserDriveChart.clean();
|
||||||
await this.apRequestChart.clean();
|
await this.apRequestChart.clean();
|
||||||
|
|
||||||
this.logger.succ('All charts successfully cleaned.');
|
this.logger.info('All charts successfully cleaned.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,6 @@ export class CleanProcessorService {
|
||||||
|
|
||||||
this.reversiService.cleanOutdatedGames();
|
this.reversiService.cleanOutdatedGames();
|
||||||
|
|
||||||
this.logger.succ('Cleaned.');
|
this.logger.info('Cleaned.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,6 @@ export class CleanRemoteFilesProcessorService {
|
||||||
await job.updateProgress(100 / total * deletedCount);
|
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}.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ export class DeleteAccountProcessorService {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.succ('All clips have been deleted.');
|
this.logger.info('All clips have been deleted.');
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // Delete favorites
|
{ // Delete favorites
|
||||||
|
@ -136,7 +136,7 @@ export class DeleteAccountProcessorService {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.succ('All favorites have been deleted.');
|
this.logger.info('All favorites have been deleted.');
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // Delete user relations
|
{ // Delete user relations
|
||||||
|
@ -172,7 +172,7 @@ export class DeleteAccountProcessorService {
|
||||||
muteeId: user.id,
|
muteeId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.succ('All user relations have been deleted.');
|
this.logger.info('All user relations have been deleted.');
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // Delete reactions
|
{ // 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
|
{ // 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
|
{ // Delete scheduled notes
|
||||||
|
@ -254,7 +254,7 @@ export class DeleteAccountProcessorService {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.succ('All scheduled notes deleted');
|
this.logger.info('All scheduled notes deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // Delete notes
|
{ // 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
|
{ // 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
|
{ // Delete actor logs
|
||||||
|
@ -353,7 +353,7 @@ export class DeleteAccountProcessorService {
|
||||||
await this.apLogService.deleteInboxLogs(user.id)
|
await this.apLogService.deleteInboxLogs(user.id)
|
||||||
.catch(err => this.logger.error(err, `Failed to delete AP logs for user '${user.uri}'`));
|
.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!
|
// Do this BEFORE deleting the account!
|
||||||
|
@ -379,7 +379,7 @@ export class DeleteAccountProcessorService {
|
||||||
await this.usersRepository.delete(user.id);
|
await this.usersRepository.delete(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('Account data deleted');
|
this.logger.info('Account data deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // Send email notification
|
{ // Send email notification
|
||||||
|
|
|
@ -74,6 +74,6 @@ export class DeleteDriveFilesProcessorService {
|
||||||
job.updateProgress(deletedCount / total);
|
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.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,9 +133,8 @@ export class DeliverProcessorService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError && !res.isRetryable) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (!res.isRetryable) {
|
|
||||||
// 相手が閉鎖していることを明示しているため、配送停止する
|
// 相手が閉鎖していることを明示しているため、配送停止する
|
||||||
if (job.data.isSharedInbox && res.statusCode === 410) {
|
if (job.data.isSharedInbox && res.statusCode === 410) {
|
||||||
this.federatedInstanceService.fetchOrRegister(host).then(i => {
|
this.federatedInstanceService.fetchOrRegister(host).then(i => {
|
||||||
|
@ -146,10 +145,6 @@ export class DeliverProcessorService {
|
||||||
throw new Bull.UnrecoverableError(`${host} is gone`);
|
throw new Bull.UnrecoverableError(`${host} is gone`);
|
||||||
}
|
}
|
||||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||||
}
|
|
||||||
|
|
||||||
// 5xx etc.
|
|
||||||
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
|
||||||
} else {
|
} else {
|
||||||
// DNS error, socket error, timeout ...
|
// DNS error, socket error, timeout ...
|
||||||
throw res;
|
throw res;
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { Packed } from '@/misc/json-schema.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import { EmailService } from '@/core/EmailService.js';
|
import { EmailService } from '@/core/EmailService.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
|
@ -85,21 +86,23 @@ export class ExportAccountDataProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job): Promise<void> {
|
public async process(job: Bull.Job): Promise<void> {
|
||||||
this.logger.info('Exporting Account Data...');
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneBy({ userId: job.data.user.id });
|
const profile = await this.userProfilesRepository.findOneBy({ userId: job.data.user.id });
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} has no profile`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Exporting account data for ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const [path, cleanup] = await createTempDir();
|
const [path, cleanup] = await createTempDir();
|
||||||
|
|
||||||
this.logger.info(`Temp dir is ${path}`);
|
this.logger.debug(`Temp dir is ${path}`);
|
||||||
|
|
||||||
// User Export
|
// User Export
|
||||||
|
|
||||||
|
@ -113,7 +116,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
userStream.write(text, err => {
|
userStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing user:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -145,7 +148,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
profileStream.write(text, err => {
|
profileStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing profile:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -179,7 +182,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
ipStream.write(text, err => {
|
ipStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing IPs:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -214,7 +217,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
notesStream.write(text, err => {
|
notesStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing notes:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -275,7 +278,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
followingStream.write(text, err => {
|
followingStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing following:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -345,7 +348,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
followerStream.write(text, err => {
|
followerStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing followers:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -406,7 +409,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
filesStream.write(text, err => {
|
filesStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing drive:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -432,7 +435,7 @@ export class ExportAccountDataProcessorService {
|
||||||
await this.downloadService.downloadUrl(file.url, filePath);
|
await this.downloadService.downloadUrl(file.url, filePath);
|
||||||
downloaded = true;
|
downloaded = true;
|
||||||
} catch (e) {
|
} 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) {
|
if (!downloaded) {
|
||||||
|
@ -464,7 +467,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
mutingStream.write(text, err => {
|
mutingStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing mutings:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -527,7 +530,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
blockingStream.write(text, err => {
|
blockingStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing blockings:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -589,7 +592,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
favoriteStream.write(text, err => {
|
favoriteStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing favorites:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -650,7 +653,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
antennaStream.write(text, err => {
|
antennaStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing antennas:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -708,7 +711,7 @@ export class ExportAccountDataProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
listStream.write(text, err => {
|
listStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error writing lists:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -744,12 +747,12 @@ export class ExportAccountDataProcessorService {
|
||||||
zlib: { level: 0 },
|
zlib: { level: 0 },
|
||||||
});
|
});
|
||||||
archiveStream.on('close', async () => {
|
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 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 });
|
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();
|
cleanup();
|
||||||
archiveCleanup();
|
archiveCleanup();
|
||||||
if (profile.email) {
|
if (profile.email) {
|
||||||
|
|
|
@ -45,15 +45,19 @@ export class ExportAntennasProcessorService {
|
||||||
public async process(job: Bull.Job<DBExportAntennasData>): Promise<void> {
|
public async process(job: Bull.Job<DBExportAntennasData>): Promise<void> {
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Exporting antennas of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||||
const write = (input: string): Promise<void> => {
|
const write = (input: string): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
stream.write(input, err => {
|
stream.write(input, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error exporting antennas:', err);
|
||||||
reject();
|
reject();
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -96,7 +100,7 @@ export class ExportAntennasProcessorService {
|
||||||
|
|
||||||
const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
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' });
|
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', {
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
exportedEntity: 'antenna',
|
exportedEntity: 'antenna',
|
||||||
|
|
|
@ -40,17 +40,18 @@ export class ExportBlockingProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting blocking of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Exporting blocking of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`Temp file is ${path}`);
|
this.logger.debug(`Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||||
|
@ -87,7 +88,7 @@ export class ExportBlockingProcessorService {
|
||||||
await new Promise<void>((res, rej) => {
|
await new Promise<void>((res, rej) => {
|
||||||
stream.write(content + '\n', err => {
|
stream.write(content + '\n', err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error exporting blocking:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -105,12 +106,12 @@ export class ExportBlockingProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.end();
|
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 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' });
|
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', {
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
exportedEntity: 'blocking',
|
exportedEntity: 'blocking',
|
||||||
|
|
|
@ -51,17 +51,18 @@ export class ExportClipsProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting clips of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Exporting clips of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`Temp file is ${path}`);
|
this.logger.debug(`Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' }));
|
const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' }));
|
||||||
|
@ -75,12 +76,12 @@ export class ExportClipsProcessorService {
|
||||||
await writer.write(']');
|
await writer.write(']');
|
||||||
await writer.close();
|
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 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' });
|
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', {
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
exportedEntity: 'clip',
|
exportedEntity: 'clip',
|
||||||
|
|
|
@ -45,16 +45,17 @@ export class ExportCustomEmojisProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job): Promise<void> {
|
public async process(job: Bull.Job): Promise<void> {
|
||||||
this.logger.info('Exporting custom emojis ...');
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Exporting custom emojis of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const [path, cleanup] = await createTempDir();
|
const [path, cleanup] = await createTempDir();
|
||||||
|
|
||||||
this.logger.info(`Temp dir is ${path}`);
|
this.logger.debug(`Temp dir is ${path}`);
|
||||||
|
|
||||||
const metaPath = path + '/meta.json';
|
const metaPath = path + '/meta.json';
|
||||||
|
|
||||||
|
@ -66,7 +67,7 @@ export class ExportCustomEmojisProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
metaStream.write(text, err => {
|
metaStream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error exporting custom emojis:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -101,7 +102,7 @@ export class ExportCustomEmojisProcessorService {
|
||||||
await this.downloadService.downloadUrl(emoji.originalUrl, emojiPath);
|
await this.downloadService.downloadUrl(emoji.originalUrl, emojiPath);
|
||||||
downloaded = true;
|
downloaded = true;
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} 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) {
|
if (!downloaded) {
|
||||||
|
@ -130,12 +131,12 @@ export class ExportCustomEmojisProcessorService {
|
||||||
zlib: { level: 0 },
|
zlib: { level: 0 },
|
||||||
});
|
});
|
||||||
archiveStream.on('close', async () => {
|
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 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 });
|
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', {
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
exportedEntity: 'customEmoji',
|
exportedEntity: 'customEmoji',
|
||||||
|
|
|
@ -45,17 +45,18 @@ export class ExportFavoritesProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting favorites of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Exporting favorites of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`Temp file is ${path}`);
|
this.logger.debug(`Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||||
|
@ -64,7 +65,7 @@ export class ExportFavoritesProcessorService {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
stream.write(text, err => {
|
stream.write(text, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error exporting favorites:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -119,12 +120,12 @@ export class ExportFavoritesProcessorService {
|
||||||
await write(']');
|
await write(']');
|
||||||
|
|
||||||
stream.end();
|
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 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' });
|
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', {
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
exportedEntity: 'favorite',
|
exportedEntity: 'favorite',
|
||||||
|
|
|
@ -44,17 +44,18 @@ export class ExportFollowingProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbExportFollowingData>): Promise<void> {
|
public async process(job: Bull.Job<DbExportFollowingData>): Promise<void> {
|
||||||
this.logger.info(`Exporting following of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Exporting following of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`Temp file is ${path}`);
|
this.logger.debug(`Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||||
|
@ -98,7 +99,7 @@ export class ExportFollowingProcessorService {
|
||||||
await new Promise<void>((res, rej) => {
|
await new Promise<void>((res, rej) => {
|
||||||
stream.write(content + '\n', err => {
|
stream.write(content + '\n', err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error exporting following:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -109,12 +110,12 @@ export class ExportFollowingProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.end();
|
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 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' });
|
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', {
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
exportedEntity: 'following',
|
exportedEntity: 'following',
|
||||||
|
|
|
@ -40,17 +40,18 @@ export class ExportMutingProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting muting of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Exporting muting of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`Temp file is ${path}`);
|
this.logger.debug(`Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||||
|
@ -88,7 +89,7 @@ export class ExportMutingProcessorService {
|
||||||
await new Promise<void>((res, rej) => {
|
await new Promise<void>((res, rej) => {
|
||||||
stream.write(content + '\n', err => {
|
stream.write(content + '\n', err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error exporting mutings:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -106,12 +107,12 @@ export class ExportMutingProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.end();
|
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 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' });
|
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', {
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
exportedEntity: 'muting',
|
exportedEntity: 'muting',
|
||||||
|
|
|
@ -120,17 +120,18 @@ export class ExportNotesProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting notes of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Exporting notes of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`Temp file is ${path}`);
|
this.logger.debug(`Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// メモリが足りなくならないようにストリームで処理する
|
// メモリが足りなくならないようにストリームで処理する
|
||||||
|
@ -146,12 +147,12 @@ export class ExportNotesProcessorService {
|
||||||
.pipeThrough(new TextEncoderStream())
|
.pipeThrough(new TextEncoderStream())
|
||||||
.pipeTo(new FileWriterStream(path));
|
.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 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' });
|
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', {
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
exportedEntity: 'note',
|
exportedEntity: 'note',
|
||||||
|
|
|
@ -43,13 +43,14 @@ export class ExportUserListsProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Exporting user lists of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Exporting user lists of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const lists = await this.userListsRepository.findBy({
|
const lists = await this.userListsRepository.findBy({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
@ -57,7 +58,7 @@ export class ExportUserListsProcessorService {
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`Temp file is ${path}`);
|
this.logger.debug(`Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||||
|
@ -74,7 +75,7 @@ export class ExportUserListsProcessorService {
|
||||||
await new Promise<void>((res, rej) => {
|
await new Promise<void>((res, rej) => {
|
||||||
stream.write(content + '\n', err => {
|
stream.write(content + '\n', err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error exporting lists:', err);
|
||||||
rej(err);
|
rej(err);
|
||||||
} else {
|
} else {
|
||||||
res();
|
res();
|
||||||
|
@ -85,12 +86,12 @@ export class ExportUserListsProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.end();
|
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 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' });
|
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', {
|
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||||
exportedEntity: 'userList',
|
exportedEntity: 'userList',
|
||||||
|
|
|
@ -8,7 +8,7 @@ import _Ajv from 'ajv';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import Logger from '@/logger.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 { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
@ -59,6 +59,9 @@ export class ImportAntennasProcessorService {
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
@ -68,12 +71,20 @@ export class ImportAntennasProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DBAntennaImportJobData>): Promise<void> {
|
public async process(job: Bull.Job<DBAntennaImportJobData>): Promise<void> {
|
||||||
|
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();
|
const now = new Date();
|
||||||
try {
|
try {
|
||||||
for (const antenna of job.data.antenna) {
|
for (const antenna of job.data.antenna) {
|
||||||
if (antenna.keywords.length === 0 || antenna.keywords[0].every(x => x === '')) continue;
|
if (antenna.keywords.length === 0 || antenna.keywords[0].every(x => x === '')) continue;
|
||||||
if (!validate(antenna)) {
|
if (!validate(antenna)) {
|
||||||
this.logger.warn('Validation Failed');
|
this.logger.warn('Antenna validation failed');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const result = await this.antennasRepository.insertOne({
|
const result = await this.antennasRepository.insertOne({
|
||||||
|
@ -92,11 +103,11 @@ export class ImportAntennasProcessorService {
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
});
|
});
|
||||||
this.logger.succ('Antenna created: ' + result.id);
|
this.logger.debug('Antenna created: ' + result.id);
|
||||||
this.globalEventService.publishInternalEvent('antennaCreated', result);
|
this.globalEventService.publishInternalEvent('antennaCreated', result);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err);
|
this.logger.error('Error importing antennas:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,10 +40,9 @@ export class ImportBlockingProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info(`Importing blocking of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,14 +50,17 @@ export class ImportBlockingProcessorService {
|
||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
|
this.logger.debug(`Skip: file ${job.data.fileId} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Importing blocking of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const csv = await this.downloadService.downloadTextFile(file.url);
|
const csv = await this.downloadService.downloadTextFile(file.url);
|
||||||
const targets = csv.trim().split('\n');
|
const targets = csv.trim().split('\n');
|
||||||
this.queueService.createImportBlockingToDbJob({ id: user.id }, targets);
|
this.queueService.createImportBlockingToDbJob({ id: user.id }, targets);
|
||||||
|
|
||||||
this.logger.succ('Import jobs created');
|
this.logger.debug('Import jobs created');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -93,11 +95,11 @@ export class ImportBlockingProcessorService {
|
||||||
// skip myself
|
// skip myself
|
||||||
if (target.id === job.data.user.id) return;
|
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 }]);
|
this.queueService.createBlockJob([{ from: { id: user.id }, to: { id: target.id }, silent: true }]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(`Error: ${e}`);
|
this.logger.error('Error importing blockings:', e as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { DriveService } from '@/core/DriveService.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbUserImportJobData } from '../types.js';
|
import type { DbUserImportJobData } from '../types.js';
|
||||||
|
@ -45,18 +46,19 @@ export class ImportCustomEmojisProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info('Importing custom emojis ...');
|
|
||||||
|
|
||||||
const file = await this.driveFilesRepository.findOneBy({
|
const file = await this.driveFilesRepository.findOneBy({
|
||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
|
this.logger.debug(`Skip: file ${job.data.fileId} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Importing custom emojis from ${file.id} (${file.name}) ...`);
|
||||||
|
|
||||||
const [path, cleanup] = await createTempDir();
|
const [path, cleanup] = await createTempDir();
|
||||||
|
|
||||||
this.logger.info(`Temp dir is ${path}`);
|
this.logger.debug(`Temp dir is ${path}`);
|
||||||
|
|
||||||
const destPath = path + '/emojis.zip';
|
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 });
|
await this.downloadService.downloadUrl(file.url, destPath, { operationTimeout: this.config.import?.downloadTimeout, maxSize: this.config.import?.maxFileSize });
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} catch (e) { // TODO: 何度か再試行
|
||||||
if (e instanceof Error || typeof e === 'string') {
|
if (e instanceof Error || typeof e === 'string') {
|
||||||
this.logger.error(e);
|
this.logger.error('Error importing custom emojis:', e as Error);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputPath = path + '/emojis';
|
const outputPath = path + '/emojis';
|
||||||
try {
|
try {
|
||||||
this.logger.succ(`Unzipping to ${outputPath}`);
|
this.logger.debug(`Unzipping to ${outputPath}`);
|
||||||
ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath));
|
ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath));
|
||||||
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
|
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
|
||||||
const meta = JSON.parse(metaRaw);
|
const meta = JSON.parse(metaRaw);
|
||||||
|
@ -117,7 +119,7 @@ export class ImportCustomEmojisProcessorService {
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error || typeof e === 'string') {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -125,11 +127,9 @@ export class ImportCustomEmojisProcessorService {
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
this.logger.succ('Imported');
|
this.logger.debug('Imported');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error || typeof e === 'string') {
|
this.logger.error('Error importing custom emojis:', e as Error);
|
||||||
this.logger.error(e);
|
|
||||||
}
|
|
||||||
cleanup();
|
cleanup();
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,10 +40,9 @@ export class ImportFollowingProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info(`Importing following of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,14 +50,17 @@ export class ImportFollowingProcessorService {
|
||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
|
this.logger.debug(`Skip: file ${job.data.fileId} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Importing following of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const csv = await this.downloadService.downloadTextFile(file.url);
|
const csv = await this.downloadService.downloadTextFile(file.url);
|
||||||
const targets = csv.trim().split('\n');
|
const targets = csv.trim().split('\n');
|
||||||
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets, job.data.withReplies);
|
this.queueService.createImportFollowingToDbJob({ id: user.id }, targets, job.data.withReplies);
|
||||||
|
|
||||||
this.logger.succ('Import jobs created');
|
this.logger.debug('Import jobs created');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -93,11 +95,11 @@ export class ImportFollowingProcessorService {
|
||||||
// skip myself
|
// skip myself
|
||||||
if (target.id === job.data.user.id) return;
|
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 }]);
|
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(`Error: ${e}`);
|
this.logger.error('Error importing followings:', e as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbUserImportJobData } from '../types.js';
|
import type { DbUserImportJobData } from '../types.js';
|
||||||
|
@ -40,10 +41,9 @@ export class ImportMutingProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info(`Importing muting of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,9 +51,12 @@ export class ImportMutingProcessorService {
|
||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
|
this.logger.debug(`Skip: file ${job.data.fileId} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Importing muting of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const csv = await this.downloadService.downloadTextFile(file.url);
|
const csv = await this.downloadService.downloadTextFile(file.url);
|
||||||
|
|
||||||
let linenum = 0;
|
let linenum = 0;
|
||||||
|
@ -88,14 +91,14 @@ export class ImportMutingProcessorService {
|
||||||
// skip myself
|
// skip myself
|
||||||
if (target.id === job.data.user.id) continue;
|
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);
|
await this.userMutingService.mute(user, target);
|
||||||
} catch (e) {
|
} 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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,10 +159,9 @@ export class ImportNotesProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbNoteImportJobData>): Promise<void> {
|
public async process(job: Bull.Job<DbNoteImportJobData>): Promise<void> {
|
||||||
this.logger.info(`Starting note import of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,9 +169,12 @@ export class ImportNotesProcessorService {
|
||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
|
this.logger.debug(`Skip: file ${job.data.fileId} does not exist`);
|
||||||
return;
|
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 });
|
let folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
||||||
if (folder == null) {
|
if (folder == null) {
|
||||||
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Imports', userId: job.data.user.id });
|
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')) {
|
if (type === 'Twitter' || file.name.startsWith('twitter') && file.name.endsWith('.zip')) {
|
||||||
const [path, cleanup] = await createTempDir();
|
const [path, cleanup] = await createTempDir();
|
||||||
|
|
||||||
this.logger.info(`Temp dir is ${path}`);
|
this.logger.debug(`Temp dir is ${path}`);
|
||||||
|
|
||||||
const destPath = path + '/twitter.zip';
|
const destPath = path + '/twitter.zip';
|
||||||
|
|
||||||
|
@ -192,15 +194,13 @@ export class ImportNotesProcessorService {
|
||||||
await fsp.writeFile(destPath, '', 'binary');
|
await fsp.writeFile(destPath, '', 'binary');
|
||||||
await this.downloadUrl(file.url, destPath);
|
await this.downloadUrl(file.url, destPath);
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} catch (e) { // TODO: 何度か再試行
|
||||||
if (e instanceof Error || typeof e === 'string') {
|
this.logger.error('Error importing notes:', e as Error);
|
||||||
this.logger.error(e);
|
|
||||||
}
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputPath = path + '/twitter';
|
const outputPath = path + '/twitter';
|
||||||
try {
|
try {
|
||||||
this.logger.succ(`Unzipping to ${outputPath}`);
|
this.logger.debug(`Unzipping to ${outputPath}`);
|
||||||
ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath));
|
ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath));
|
||||||
|
|
||||||
const unprocessedTweets = this.parseTwitterFile(await fsp.readFile(outputPath + '/data/tweets.js', 'utf-8'));
|
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')) {
|
} else if (type === 'Facebook' || file.name.startsWith('facebook-') && file.name.endsWith('.zip')) {
|
||||||
const [path, cleanup] = await createTempDir();
|
const [path, cleanup] = await createTempDir();
|
||||||
|
|
||||||
this.logger.info(`Temp dir is ${path}`);
|
this.logger.debug(`Temp dir is ${path}`);
|
||||||
|
|
||||||
const destPath = path + '/facebook.zip';
|
const destPath = path + '/facebook.zip';
|
||||||
|
|
||||||
|
@ -222,15 +222,13 @@ export class ImportNotesProcessorService {
|
||||||
await fsp.writeFile(destPath, '', 'binary');
|
await fsp.writeFile(destPath, '', 'binary');
|
||||||
await this.downloadUrl(file.url, destPath);
|
await this.downloadUrl(file.url, destPath);
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} catch (e) { // TODO: 何度か再試行
|
||||||
if (e instanceof Error || typeof e === 'string') {
|
this.logger.error('Error importing notes:', e as Error);
|
||||||
this.logger.error(e);
|
|
||||||
}
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputPath = path + '/facebook';
|
const outputPath = path + '/facebook';
|
||||||
try {
|
try {
|
||||||
this.logger.succ(`Unzipping to ${outputPath}`);
|
this.logger.debug(`Unzipping to ${outputPath}`);
|
||||||
ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath));
|
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 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);
|
const posts = JSON.parse(postsJson);
|
||||||
|
@ -247,7 +245,7 @@ export class ImportNotesProcessorService {
|
||||||
} else if (file.name.endsWith('.zip')) {
|
} else if (file.name.endsWith('.zip')) {
|
||||||
const [path, cleanup] = await createTempDir();
|
const [path, cleanup] = await createTempDir();
|
||||||
|
|
||||||
this.logger.info(`Temp dir is ${path}`);
|
this.logger.debug(`Temp dir is ${path}`);
|
||||||
|
|
||||||
const destPath = path + '/unknown.zip';
|
const destPath = path + '/unknown.zip';
|
||||||
|
|
||||||
|
@ -255,15 +253,13 @@ export class ImportNotesProcessorService {
|
||||||
await fsp.writeFile(destPath, '', 'binary');
|
await fsp.writeFile(destPath, '', 'binary');
|
||||||
await this.downloadUrl(file.url, destPath);
|
await this.downloadUrl(file.url, destPath);
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} catch (e) { // TODO: 何度か再試行
|
||||||
if (e instanceof Error || typeof e === 'string') {
|
this.logger.error('Error importing notes:', e as Error);
|
||||||
this.logger.error(e);
|
|
||||||
}
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputPath = path + '/unknown';
|
const outputPath = path + '/unknown';
|
||||||
try {
|
try {
|
||||||
this.logger.succ(`Unzipping to ${outputPath}`);
|
this.logger.debug(`Unzipping to ${outputPath}`);
|
||||||
ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath));
|
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 isInstagram = type === 'Instagram' || fs.existsSync(outputPath + '/instagram_live') || fs.existsSync(outputPath + '/instagram_ads_and_businesses');
|
||||||
const isOutbox = type === 'Mastodon' || fs.existsSync(outputPath + '/outbox.json');
|
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')) {
|
} else if (job.data.type === 'Misskey' || file.name.startsWith('notes-') && file.name.endsWith('.json')) {
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
this.logger.info(`Temp dir is ${path}`);
|
this.logger.debug(`Temp dir is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fsp.writeFile(path, '', 'utf-8');
|
await fsp.writeFile(path, '', 'utf-8');
|
||||||
await this.downloadUrl(file.url, path);
|
await this.downloadUrl(file.url, path);
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} catch (e) { // TODO: 何度か再試行
|
||||||
if (e instanceof Error || typeof e === 'string') {
|
this.logger.error('Error importing notes:', e as Error);
|
||||||
this.logger.error(e);
|
|
||||||
}
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,7 +320,7 @@ export class ImportNotesProcessorService {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('Import jobs created');
|
this.logger.debug('Import jobs created');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -365,7 +359,7 @@ export class ImportNotesProcessorService {
|
||||||
try {
|
try {
|
||||||
await this.downloadUrl(file.url, filePath);
|
await this.downloadUrl(file.url, filePath);
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} 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({
|
const driveFile = await this.driveService.addFile({
|
||||||
user: user,
|
user: user,
|
||||||
|
@ -504,7 +498,7 @@ export class ImportNotesProcessorService {
|
||||||
try {
|
try {
|
||||||
await this.downloadUrl(file.url, filePath);
|
await this.downloadUrl(file.url, filePath);
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} 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({
|
const driveFile = await this.driveService.addFile({
|
||||||
user: user,
|
user: user,
|
||||||
|
@ -628,7 +622,7 @@ export class ImportNotesProcessorService {
|
||||||
try {
|
try {
|
||||||
await this.downloadUrl(videos[0].url, filePath);
|
await this.downloadUrl(videos[0].url, filePath);
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} 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({
|
const driveFile = await this.driveService.addFile({
|
||||||
user: user,
|
user: user,
|
||||||
|
@ -653,7 +647,7 @@ export class ImportNotesProcessorService {
|
||||||
try {
|
try {
|
||||||
await this.downloadUrl(file.media_url_https, filePath);
|
await this.downloadUrl(file.media_url_https, filePath);
|
||||||
} catch (e) { // TODO: 何度か再試行
|
} 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({
|
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 });
|
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);
|
if (tweet.childNotes) this.queueService.createImportTweetsToDbJob(user, tweet.childNotes, createdNote.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(`Error: ${e}`);
|
this.logger.error('Error importing notes:', e as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { UserListService } from '@/core/UserListService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbUserImportJobData } from '../types.js';
|
import type { DbUserImportJobData } from '../types.js';
|
||||||
|
@ -48,10 +49,9 @@ export class ImportUserListsProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
public async process(job: Bull.Job<DbUserImportJobData>): Promise<void> {
|
||||||
this.logger.info(`Importing user lists of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.logger.debug(`Skip: user ${job.data.user.id} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,9 +59,12 @@ export class ImportUserListsProcessorService {
|
||||||
id: job.data.fileId,
|
id: job.data.fileId,
|
||||||
});
|
});
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
|
this.logger.debug(`Skip: file ${job.data.fileId} does not exist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Importing user lists of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const csv = await this.downloadService.downloadTextFile(file.url);
|
const csv = await this.downloadService.downloadTextFile(file.url);
|
||||||
|
|
||||||
let linenum = 0;
|
let linenum = 0;
|
||||||
|
@ -102,10 +105,10 @@ export class ImportUserListsProcessorService {
|
||||||
|
|
||||||
this.userListService.addMember(target, list!, user);
|
this.userListService.addMember(target, list!, user);
|
||||||
} catch (e) {
|
} 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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ import { SkApInboxLog } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { ApLogService, calculateDurationSince } from '@/core/ApLogService.js';
|
import { ApLogService, calculateDurationSince } from '@/core/ApLogService.js';
|
||||||
import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.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 { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type { InboxJobData } from '../types.js';
|
import type { InboxJobData } from '../types.js';
|
||||||
|
|
||||||
|
@ -145,12 +147,11 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
authUser = await this.apDbResolverService.getAuthUserFromApId(actorId);
|
authUser = await this.apDbResolverService.getAuthUserFromApId(actorId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 対象が4xxならスキップ
|
// 対象が4xxならスキップ
|
||||||
if (err instanceof StatusError) {
|
if (!isRetryableError(err)) {
|
||||||
if (!err.isRetryable) {
|
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${actorId}`);
|
||||||
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${actorId} - ${err.statusCode}`);
|
|
||||||
}
|
|
||||||
throw new Error(`Error in actor ${actorId} - ${err.statusCode}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,7 +228,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
||||||
if (!this.utilityService.isFederationAllowedHost(ldHost)) {
|
if (!this.utilityService.isFederationAllowedHost(ldHost)) {
|
||||||
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
throw new Bull.UnrecoverableError(`skip: request host is blocked: ${ldHost}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
|
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) {
|
if (!isRetryableError(e)) {
|
||||||
return `skip: permanent error ${e.statusCode}`;
|
return `skip: permanent error ${renderInlineError(e)}`;
|
||||||
}
|
|
||||||
|
|
||||||
if (e instanceof IdentifiableError && !e.isRetryable) {
|
|
||||||
if (e.message) {
|
|
||||||
return `skip: permanent error ${e.id}: ${e.message}`;
|
|
||||||
} else {
|
|
||||||
return `skip: permanent error ${e.id}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
@ -36,6 +36,6 @@ export class ResyncChartsProcessorService {
|
||||||
await this.notesChart.resync();
|
await this.notesChart.resync();
|
||||||
await this.usersChart.resync();
|
await this.usersChart.resync();
|
||||||
|
|
||||||
this.logger.succ('All charts successfully resynced.');
|
this.logger.info('All charts successfully resynced.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type { MiScheduleNoteType } from '@/models/NoteSchedule.js';
|
import type { MiScheduleNoteType } from '@/models/NoteSchedule.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import type { ScheduleNotePostJobData } from '../types.js';
|
import type { ScheduleNotePostJobData } from '../types.js';
|
||||||
|
@ -129,10 +130,11 @@ export class ScheduleNotePostProcessorService {
|
||||||
channel,
|
channel,
|
||||||
}).catch(async (err: IdentifiableError) => {
|
}).catch(async (err: IdentifiableError) => {
|
||||||
this.notificationService.createNotification(me.id, 'scheduledNoteFailed', {
|
this.notificationService.createNotification(me.id, 'scheduledNoteFailed', {
|
||||||
reason: err.message,
|
reason: renderInlineError(err),
|
||||||
});
|
});
|
||||||
await this.noteScheduleRepository.remove(data);
|
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);
|
await this.noteScheduleRepository.remove(data);
|
||||||
this.notificationService.createNotification(me.id, 'scheduledNotePosted', {
|
this.notificationService.createNotification(me.id, 'scheduledNotePosted', {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type Logger from '@/logger.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import { SystemWebhookDeliverJobData } from '../types.js';
|
import { SystemWebhookDeliverJobData } from '../types.js';
|
||||||
|
|
||||||
|
@ -63,21 +64,16 @@ export class SystemWebhookDeliverProcessorService {
|
||||||
|
|
||||||
return 'Success';
|
return 'Success';
|
||||||
} catch (res) {
|
} catch (res) {
|
||||||
this.logger.error(res as Error);
|
this.logger.error(`Failed to send webhook: ${renderInlineError(res)}`);
|
||||||
|
|
||||||
this.systemWebhooksRepository.update({ id: job.data.webhookId }, {
|
this.systemWebhooksRepository.update({ id: job.data.webhookId }, {
|
||||||
latestSentAt: new Date(),
|
latestSentAt: new Date(),
|
||||||
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError && !res.isRetryable) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (!res.isRetryable) {
|
|
||||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||||
}
|
|
||||||
|
|
||||||
// 5xx etc.
|
|
||||||
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
|
||||||
} else {
|
} else {
|
||||||
// DNS error, socket error, timeout ...
|
// DNS error, socket error, timeout ...
|
||||||
throw res;
|
throw res;
|
||||||
|
|
|
@ -62,6 +62,6 @@ export class TickChartsProcessorService {
|
||||||
await this.perUserDriveChart.tick(false);
|
await this.perUserDriveChart.tick(false);
|
||||||
await this.apRequestChart.tick(false);
|
await this.apRequestChart.tick(false);
|
||||||
|
|
||||||
this.logger.succ('All charts successfully ticked.');
|
this.logger.info('All charts successfully ticked.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,14 +69,9 @@ export class UserWebhookDeliverProcessorService {
|
||||||
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError && !res.isRetryable) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (!res.isRetryable) {
|
|
||||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||||
}
|
|
||||||
|
|
||||||
// 5xx etc.
|
|
||||||
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
|
||||||
} else {
|
} else {
|
||||||
// DNS error, socket error, timeout ...
|
// DNS error, socket error, timeout ...
|
||||||
throw res;
|
throw res;
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||||
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
||||||
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
|
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
|
||||||
import { Keyed, RateLimit, sendRateLimitHeaders } from '@/misc/rate-limit-utils.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';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
|
@ -120,7 +121,7 @@ export class FileServerService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async errorHandler(request: FastifyRequest<{ Params?: { [x: string]: any }; Querystring?: { [x: string]: any }; }>, reply: FastifyReply, err?: any) {
|
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');
|
reply.header('Cache-Control', 'max-age=300');
|
||||||
|
|
||||||
|
@ -353,7 +354,7 @@ export class FileServerService {
|
||||||
if (!request.headers['user-agent']) {
|
if (!request.headers['user-agent']) {
|
||||||
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
|
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
|
||||||
} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
|
} 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
|
// Create temp file
|
||||||
|
@ -383,7 +384,7 @@ export class FileServerService {
|
||||||
) {
|
) {
|
||||||
if (!isConvertibleImage) {
|
if (!isConvertibleImage) {
|
||||||
// 画像でないなら404でお茶を濁す
|
// 画像でないなら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') {
|
} else if (file.mime === 'image/svg+xml') {
|
||||||
image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
|
image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
|
||||||
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
|
} 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) {
|
if (!image) {
|
||||||
|
@ -521,7 +522,7 @@ export class FileServerService {
|
||||||
> {
|
> {
|
||||||
if (url.startsWith(`${this.config.url}/files/`)) {
|
if (url.startsWith(`${this.config.url}/files/`)) {
|
||||||
const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
|
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);
|
return await this.getFileFromKey(key);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
||||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||||
import { ApiServerService } from './api/ApiServerService.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.`);
|
this.logger.error(`Port ${this.config.port} is already in use by another process.`);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.logger.error(err);
|
this.logger.error(`Unhandled error in server: ${renderInlineError(err)}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -389,7 +389,7 @@ function createLimitKey(limit: ParsedLimit, actor: string, value: string): strin
|
||||||
return `rl_${actor}_${limit.key}_${value}`;
|
return `rl_${actor}_${limit.key}_${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConflictError extends Error {}
|
export class ConflictError extends Error {}
|
||||||
|
|
||||||
interface LimitCounter {
|
interface LimitCounter {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|
|
@ -20,12 +20,14 @@ import { RoleService } from '@/core/RoleService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
|
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
|
||||||
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
|
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { ApiError } from './error.js';
|
import { ApiError } from './error.js';
|
||||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
import type { IEndpointMeta, IEndpoint } from './endpoints.js';
|
import type { IEndpointMeta, IEndpoint } from './endpoints.js';
|
||||||
|
import { renderFullError } from '@/misc/render-full-error.js';
|
||||||
|
|
||||||
const accessDenied = {
|
const accessDenied = {
|
||||||
message: 'Access denied.',
|
message: 'Access denied.',
|
||||||
|
@ -100,26 +102,26 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
throw err;
|
throw err;
|
||||||
} else {
|
} else {
|
||||||
const errId = randomUUID();
|
const errId = randomUUID();
|
||||||
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
const fullError = renderFullError(err);
|
||||||
ep: ep.name,
|
const message = typeof(fullError) === 'string'
|
||||||
ps: data,
|
? `Internal error id=${errId} occurred in ${ep.name}: ${fullError}`
|
||||||
e: {
|
: `Internal error id=${errId} occurred in ${ep.name}:`;
|
||||||
message: err.message,
|
const data = typeof(fullError) === 'object'
|
||||||
code: err.name,
|
? { e: fullError }
|
||||||
stack: err.stack,
|
: {};
|
||||||
id: errId,
|
this.logger.error(message, {
|
||||||
},
|
user: userId ?? '<unauthenticated>',
|
||||||
|
...data,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.config.sentryForBackend) {
|
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',
|
level: 'error',
|
||||||
user: {
|
user: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
extra: {
|
extra: {
|
||||||
ep: ep.name,
|
ep: ep.name,
|
||||||
ps: data,
|
|
||||||
e: {
|
e: {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
code: err.name,
|
code: err.name,
|
||||||
|
|
|
@ -36,7 +36,7 @@ export class GetterService {
|
||||||
const note = await this.notesRepository.findOneBy({ id: noteId });
|
const note = await this.notesRepository.findOneBy({ id: noteId });
|
||||||
|
|
||||||
if (note == null) {
|
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;
|
return note;
|
||||||
|
@ -47,7 +47,7 @@ export class GetterService {
|
||||||
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
|
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
|
||||||
|
|
||||||
if (note == null) {
|
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;
|
return note;
|
||||||
|
@ -59,7 +59,7 @@ export class GetterService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getEdits(noteId: MiNote['id']) {
|
public async getEdits(noteId: MiNote['id']) {
|
||||||
const edits = await this.noteEditRepository.findBy({ noteId: noteId }).catch(() => {
|
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;
|
return edits;
|
||||||
|
@ -73,7 +73,7 @@ export class GetterService {
|
||||||
const user = await this.usersRepository.findOneBy({ id: userId });
|
const user = await this.usersRepository.findOneBy({ id: userId });
|
||||||
|
|
||||||
if (user == null) {
|
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;
|
return user as MiLocalUser | MiRemoteUser;
|
||||||
|
|
|
@ -205,37 +205,37 @@ export class SigninApiService {
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
|
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
|
||||||
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
|
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) {
|
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 => {
|
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) {
|
if (this.meta.enableFC && this.meta.fcSecretKey) {
|
||||||
await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => {
|
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) {
|
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
|
||||||
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
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) {
|
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
|
||||||
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
|
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) {
|
if (this.meta.enableTestcaptcha) {
|
||||||
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
|
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
|
||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, String(err), err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ export class SigninWithPasskeyApiService {
|
||||||
try {
|
try {
|
||||||
authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential);
|
authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential);
|
||||||
} catch (err) {
|
} 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;
|
const errorId = (err as IdentifiableError).id;
|
||||||
return error(403, {
|
return error(403, {
|
||||||
id: errorId,
|
id: errorId,
|
||||||
|
|
|
@ -83,37 +83,37 @@ export class SignupApiService {
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
|
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
|
||||||
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
|
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) {
|
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 => {
|
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) {
|
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
|
||||||
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
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) {
|
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
|
||||||
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
|
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) {
|
if (this.meta.enableFC && this.meta.fcSecretKey) {
|
||||||
await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => {
|
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) {
|
if (this.meta.enableTestcaptcha) {
|
||||||
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
|
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,
|
token: secret,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} 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);
|
return this.signinService.signin(request, reply, account as MiLocalUser);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
throw new FastifyReplyError(400, String(err), err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,11 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private readonly moderationLogService: ModerationLogService,
|
private readonly moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
try {
|
if (!URL.canParse(ps.inbox)) throw new ApiError(meta.errors.invalidUrl);
|
||||||
if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
|
if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError(meta.errors.invalidUrl);
|
||||||
} catch {
|
|
||||||
throw new ApiError(meta.errors.invalidUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.moderationLogService.log(me, 'addRelay', {
|
await this.moderationLogService.log(me, 'addRelay', {
|
||||||
inbox: ps.inbox,
|
inbox: ps.inbox,
|
||||||
|
|
|
@ -173,6 +173,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
|
case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
|
||||||
throw new ApiError(meta.errors.federationNotAllowed);
|
throw new ApiError(meta.errors.federationNotAllowed);
|
||||||
case '72180409-793c-4973-868e-5a118eb5519b':
|
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);
|
throw new ApiError(meta.errors.responseInvalid);
|
||||||
|
|
||||||
// resolveLocal
|
// resolveLocal
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import type { Config } from '@/config.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 { ApiError } from '../../../error.js';
|
||||||
import { MiMeta } from '@/models/_.js';
|
import { MiMeta } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -95,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
|
private readonly apiLoggerService: ApiLoggerService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
|
super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
|
||||||
// Get 'name' parameter
|
// Get 'name' parameter
|
||||||
|
@ -130,14 +133,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return await this.driveFileEntityService.pack(driveFile, { self: true });
|
return await this.driveFileEntityService.pack(driveFile, { self: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error || typeof err === 'string') {
|
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 instanceof IdentifiableError) {
|
||||||
if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate);
|
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 === '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);
|
if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded);
|
||||||
}
|
}
|
||||||
throw new ApiError();
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
cleanup!();
|
cleanup!();
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
))).filter(x => x != null);
|
))).filter(x => x != null);
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
throw new Error();
|
throw new Error('no files specified');
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({
|
const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
))).filter(x => x != null);
|
))).filter(x => x != null);
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
throw new Error();
|
throw new Error('no files');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
try {
|
try {
|
||||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('authentication failed');
|
throw new Error('authentication failed', { cause: e });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MiMeta } from '@/models/_.js';
|
import { MiMeta } from '@/models/_.js';
|
||||||
|
@ -105,7 +105,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const { username, host } = Acct.parse(ps.moveToAccount);
|
const { username, host } = Acct.parse(ps.moveToAccount);
|
||||||
// retrieve the destination account
|
// retrieve the destination account
|
||||||
let moveTo = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => {
|
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);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
});
|
});
|
||||||
const destination = await this.getterService.getUser(moveTo.id) as MiLocalUser | MiRemoteUser;
|
const destination = await this.getterService.getUser(moveTo.id) as MiLocalUser | MiRemoteUser;
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { verifyFieldLinks } from '@/misc/verify-field-link.js';
|
||||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||||
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
||||||
import { userUnsignedFetchOptions } from '@/const.js';
|
import { userUnsignedFetchOptions } from '@/const.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
@ -516,7 +517,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
// Retrieve the old account
|
// Retrieve the old account
|
||||||
const knownAs = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => {
|
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);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
});
|
});
|
||||||
if (knownAs.id === _user.id) throw new ApiError(meta.errors.forbiddenToSetYourself);
|
if (knownAs.id === _user.id) throw new ApiError(meta.errors.forbiddenToSetYourself);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
|
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
|
@ -131,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
// Lookup user
|
// Lookup user
|
||||||
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
|
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
|
||||||
user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host).catch(err => {
|
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);
|
throw new ApiError(meta.errors.failedToResolveRemoteUser);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,6 +8,9 @@ import { AbortError } from 'node-fetch';
|
||||||
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-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, () => {
|
describe(isRetryableError, () => {
|
||||||
it('should return true for retryable StatusError', () => {
|
it('should return true for retryable StatusError', () => {
|
||||||
|
@ -55,6 +58,78 @@ describe(isRetryableError, () => {
|
||||||
expect(result).toBeTruthy();
|
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 = [
|
const nonErrorInputs = [
|
||||||
[null, 'null'],
|
[null, 'null'],
|
||||||
[undefined, 'undefined'],
|
[undefined, 'undefined'],
|
||||||
|
|
|
@ -28,7 +28,7 @@ console.log('Sharkey Embed');
|
||||||
//#region Embedパラメータの取得・パース
|
//#region Embedパラメータの取得・パース
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const embedParams = parseEmbedParams(params);
|
const embedParams = parseEmbedParams(params);
|
||||||
if (_DEV_) console.log(embedParams);
|
if (_DEV_) console.debug(embedParams);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region テーマ
|
//#region テーマ
|
||||||
|
|
|
@ -28,7 +28,7 @@ let defaultIframeId: string | null = null;
|
||||||
export function setIframeId(id: string): void {
|
export function setIframeId(id: string): void {
|
||||||
if (defaultIframeId != null) return;
|
if (defaultIframeId != null) return;
|
||||||
|
|
||||||
if (_DEV_) console.log('setIframeId', id);
|
if (_DEV_) console.debug('setIframeId', id);
|
||||||
defaultIframeId = id;
|
defaultIframeId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ export function postMessageToParentWindow<T extends PostMessageEventType = PostM
|
||||||
if (_iframeId == null) {
|
if (_iframeId == null) {
|
||||||
_iframeId = defaultIframeId;
|
_iframeId = defaultIframeId;
|
||||||
}
|
}
|
||||||
if (_DEV_) console.log('postMessageToParentWindow', type, _iframeId, payload);
|
if (_DEV_) console.debug('postMessageToParentWindow', type, _iframeId, payload);
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type,
|
type,
|
||||||
iframeId: _iframeId,
|
iframeId: _iframeId,
|
||||||
|
|
|
@ -54,7 +54,7 @@ function safeURIDecode(str: string): string {
|
||||||
|
|
||||||
const page = location.pathname.split('/')[2];
|
const page = location.pathname.split('/')[2];
|
||||||
const contentId = safeURIDecode(location.pathname.split('/')[3]);
|
const contentId = safeURIDecode(location.pathname.split('/')[3]);
|
||||||
if (_DEV_) console.log(page, contentId);
|
if (_DEV_) console.debug(page, contentId);
|
||||||
|
|
||||||
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ try {
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(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
|
// Fallback to en-US
|
||||||
_dateTimeFormat = new Intl.DateTimeFormat('en-US', {
|
_dateTimeFormat = new Intl.DateTimeFormat('en-US', {
|
||||||
|
@ -43,7 +43,7 @@ try {
|
||||||
_numberFormat = new Intl.NumberFormat(versatileLang);
|
_numberFormat = new Intl.NumberFormat(versatileLang);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(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
|
// Fallback to en-US
|
||||||
_numberFormat = new Intl.NumberFormat('en-US');
|
_numberFormat = new Intl.NumberFormat('en-US');
|
||||||
|
|
|
@ -28,13 +28,13 @@ export class WorkerMultiDispatch<POST = unknown, RETURN = unknown> {
|
||||||
});
|
});
|
||||||
this.finalizationRegistry.register(this, this.symbol);
|
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) {
|
public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: WorkerNumberGetter = this.getUseWorkerNumber) {
|
||||||
let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
|
let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
|
||||||
workerNumber = Math.abs(Math.round(workerNumber)) % 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;
|
this.prevWorkerNumber = workerNumber;
|
||||||
|
|
||||||
// 不毛だがunionをoverloadに突っ込めない
|
// 不毛だがunionをoverloadに突っ込めない
|
||||||
|
@ -64,7 +64,7 @@ export class WorkerMultiDispatch<POST = unknown, RETURN = unknown> {
|
||||||
|
|
||||||
public terminate() {
|
public terminate() {
|
||||||
this.terminated = true;
|
this.terminated = true;
|
||||||
if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this);
|
if (_DEV_) console.debug('WorkerMultiDispatch: Terminating', this);
|
||||||
this.workers.forEach(worker => {
|
this.workers.forEach(worker => {
|
||||||
worker.terminate();
|
worker.terminate();
|
||||||
});
|
});
|
||||||
|
|
|
@ -142,7 +142,7 @@ function reset() {
|
||||||
function remove() {
|
function remove() {
|
||||||
if (captcha.value.remove && captchaWidgetId.value) {
|
if (captcha.value.remove && captchaWidgetId.value) {
|
||||||
try {
|
try {
|
||||||
if (_DEV_) console.log('remove', props.provider, captchaWidgetId.value);
|
if (_DEV_) console.debug('remove', props.provider, captchaWidgetId.value);
|
||||||
captcha.value.remove(captchaWidgetId.value);
|
captcha.value.remove(captchaWidgetId.value);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// ignore
|
// ignore
|
||||||
|
|
|
@ -52,7 +52,7 @@ async function fetchLanguage(to: string): Promise<void> {
|
||||||
return bundle.id === language || bundle.aliases?.includes(language);
|
return bundle.id === language || bundle.aliases?.includes(language);
|
||||||
});
|
});
|
||||||
if (bundles.length > 0) {
|
if (bundles.length > 0) {
|
||||||
if (_DEV_) console.log(`Loading language: ${language}`);
|
if (_DEV_) console.debug(`Loading language: ${language}`);
|
||||||
await highlighter.loadLanguage(bundles[0].import);
|
await highlighter.loadLanguage(bundles[0].import);
|
||||||
codeLang.value = language;
|
codeLang.value = language;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -106,7 +106,7 @@ windowRouter.addListener('replace', ctx => {
|
||||||
});
|
});
|
||||||
|
|
||||||
windowRouter.addListener('change', 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);
|
searchMarkerId.value = getSearchMarker(ctx.fullPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -386,7 +386,7 @@ function prepend(item: MisskeyEntity): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_DEV_) console.log(isHead(), isPausingUpdate);
|
if (_DEV_) console.debug(isHead(), isPausingUpdate);
|
||||||
|
|
||||||
if (isHead() && !isPausingUpdate) unshiftItems([item]);
|
if (isHead() && !isPausingUpdate) unshiftItems([item]);
|
||||||
else prependQueue(item);
|
else prependQueue(item);
|
||||||
|
|
|
@ -307,7 +307,7 @@ async function onSubmit(): Promise<void> {
|
||||||
emit('approvalPending');
|
emit('approvalPending');
|
||||||
} else {
|
} else {
|
||||||
const resJson = (await res.json()) as Misskey.entities.SignupResponse;
|
const resJson = (await res.json()) as Misskey.entities.SignupResponse;
|
||||||
if (_DEV_) console.log(resJson);
|
if (_DEV_) console.debug(resJson);
|
||||||
|
|
||||||
emit('signup', resJson);
|
emit('signup', resJson);
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue