Merge pull request 'refactor: 🎨 rome' (#9464) from refactor/backend-errors-begone into develop

Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9464
This commit is contained in:
Kainoa Kanter 2023-01-13 05:00:56 +00:00
commit 217a37a59a
975 changed files with 34144 additions and 25240 deletions

View File

@ -1,4 +1,4 @@
{ {
"eslint.packageManager": "pnpm", "eslint.packageManager": "pnpm",
"workspace.workspaceFolderCheckCwd": false, "workspace.workspaceFolderCheckCwd": false
} }

View File

@ -3,7 +3,8 @@ pipeline:
image: node:latest image: node:latest
commands: commands:
- cp .config/ci.yml .config/default.yml - cp .config/ci.yml .config/default.yml
- npm i -g pnpm - corepack enable
- corepack prepare pnpm@latest --activate
- pnpm i --frozen-lockfile - pnpm i --frozen-lockfile
- pnpm run build - pnpm run build
- pnpm run migrate - pnpm run migrate

View File

@ -106,6 +106,8 @@
- New post style - New post style
- Admins set default reaction emoji - Admins set default reaction emoji
- Allows custom emoji - Allows custom emoji
- Fix lint errors
- Use Rome instead of ESLint
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1) - MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996) - [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056) - [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)

View File

@ -10,7 +10,8 @@ RUN apk update
RUN apk add git ffmpeg tini alpine-sdk python3 RUN apk add git ffmpeg tini alpine-sdk python3
# Configure corepack and pnpm # Configure corepack and pnpm
RUN npm i -g pnpm RUN corepack enable
RUN corepack prepare pnpm@latest --activate
RUN pnpm i --frozen-lockfile RUN pnpm i --frozen-lockfile
ARG NODE_ENV=production ARG NODE_ENV=production

View File

@ -101,7 +101,8 @@ cd calckey/
```sh ```sh
# nvm install 19 && nvm use 19 # nvm install 19 && nvm use 19
npm i -g pnpm corepack enable
corepack prepare pnpm@latest --activate
pnpm i pnpm i
``` ```

View File

@ -15,7 +15,6 @@
/** /**
* @type {Cypress.PluginConfig} * @type {Cypress.PluginConfig}
*/ */
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => { module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits // `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config // `config` is the resolved Cypress config

View File

@ -40,7 +40,6 @@
"@bull-board/ui": "^4.10.2", "@bull-board/ui": "^4.10.2",
"@tensorflow/tfjs": "^3.21.0", "@tensorflow/tfjs": "^3.21.0",
"calckey-js": "^0.0.20", "calckey-js": "^0.0.20",
"eslint": "^8.31.0",
"execa": "5.1.1", "execa": "5.1.1",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
@ -55,12 +54,11 @@
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/parser": "5.46.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "10.11.0", "cypress": "10.11.0",
"install-peers": "^1.0.4", "install-peers": "^1.0.4",
"rome": "^11.0.0",
"start-server-and-test": "1.15.2", "start-server-and-test": "1.15.2",
"typescript": "4.9.4", "typescript": "4.9.4"
"vue-eslint-parser": "^9.1.0"
} }
} }

View File

@ -10,7 +10,7 @@
"revertmigration": "typeorm migration:revert -d ormconfig.js", "revertmigration": "typeorm migration:revert -d ormconfig.js",
"build": "pnpm swc src -d built -D", "build": "pnpm swc src -d built -D",
"watch": "pnpm swc src -d built -D -w", "watch": "pnpm swc src -d built -D -w",
"lint": "eslint --quiet \"src/**/*.ts\"", "lint": "pnpm rome check \"src/**/*.ts\"",
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
"test": "pnpm run mocha" "test": "pnpm run mocha"
}, },
@ -31,7 +31,10 @@
"@koa/multer": "3.0.0", "@koa/multer": "3.0.0",
"@koa/router": "9.0.1", "@koa/router": "9.0.1",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@redocly/openapi-core": "1.0.0-beta.120",
"@sinonjs/fake-timers": "9.1.2", "@sinonjs/fake-timers": "9.1.2",
"@swc/cli": "^0.1.59",
"@swc/core": "^1.3.26",
"@syuilo/aiscript": "0.11.1", "@syuilo/aiscript": "0.11.1",
"@tensorflow/tfjs": "^4.2.0", "@tensorflow/tfjs": "^4.2.0",
"ajv": "8.11.2", "ajv": "8.11.2",
@ -132,9 +135,6 @@
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.120",
"@swc/cli": "^0.1.59",
"@swc/core": "^1.3.26",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.9", "@types/bull": "3.15.9",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
@ -178,11 +178,8 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.3", "@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.31.0", "eslint": "^8.31.0",
"eslint-plugin-import": "2.26.0",
"execa": "6.1.0", "execa": "6.1.0",
"swc-loader": "^0.2.3", "swc-loader": "^0.2.3",
"typescript": "4.9.4", "typescript": "4.9.4",

View File

@ -1,11 +1,14 @@
declare module 'hcaptcha' { declare module "hcaptcha" {
interface IVerifyResponse { interface IVerifyResponse {
success: boolean; success: boolean;
challenge_ts: string; challenge_ts: string;
hostname: string; hostname: string;
credit?: boolean; credit?: boolean;
'error-codes'?: unknown[]; "error-codes"?: unknown[];
} }
export function verify(secret: string, token: string): Promise<IVerifyResponse>; export function verify(
secret: string,
token: string,
): Promise<IVerifyResponse>;
} }

View File

@ -1,5 +1,5 @@
declare module '@peertube/http-signature' { declare module "@peertube/http-signature" {
import { IncomingMessage, ClientRequest } from 'node:http'; import type { IncomingMessage, ClientRequest } from "node:http";
interface ISignature { interface ISignature {
keyId: string; keyId: string;
@ -28,8 +28,8 @@ declare module '@peertube/http-signature' {
} }
type RequestSignerConstructorOptions = type RequestSignerConstructorOptions =
IRequestSignerConstructorOptionsFromProperties | | IRequestSignerConstructorOptionsFromProperties
IRequestSignerConstructorOptionsFromFunction; | IRequestSignerConstructorOptionsFromFunction;
interface IRequestSignerConstructorOptionsFromProperties { interface IRequestSignerConstructorOptionsFromProperties {
keyId: string; keyId: string;
@ -59,11 +59,23 @@ declare module '@peertube/http-signature' {
httpVersion?: string; httpVersion?: string;
} }
export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; export function parse(
export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; request: IncomingMessage,
options?: IParseRequestOptions,
): IParsedSignature;
export function parseRequest(
request: IncomingMessage,
options?: IParseRequestOptions,
): IParsedSignature;
export function sign(request: ClientRequest, options: ISignRequestOptions): boolean; export function sign(
export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean; request: ClientRequest,
options: ISignRequestOptions,
): boolean;
export function signRequest(
request: ClientRequest,
options: ISignRequestOptions,
): boolean;
export function createSigner(): RequestSigner; export function createSigner(): RequestSigner;
export function isSigner(obj: any): obj is RequestSigner; export function isSigner(obj: any): obj is RequestSigner;
@ -71,7 +83,16 @@ declare module '@peertube/http-signature' {
export function sshKeyFingerprint(key: string): string; export function sshKeyFingerprint(key: string): string;
export function pemToRsaSSHKey(pem: string, comment: string): string; export function pemToRsaSSHKey(pem: string, comment: string): string;
export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; export function verify(
export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; parsedSignature: IParsedSignature,
export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean; pubkey: string | Buffer,
): boolean;
export function verifySignature(
parsedSignature: IParsedSignature,
pubkey: string | Buffer,
): boolean;
export function verifyHMAC(
parsedSignature: IParsedSignature,
secret: string,
): boolean;
} }

View File

@ -1,5 +1,5 @@
declare module 'koa-json-body' { declare module "koa-json-body" {
import { Middleware } from 'koa'; import type { Middleware } from "koa";
interface IKoaJsonBodyOptions { interface IKoaJsonBodyOptions {
strict: boolean; strict: boolean;

View File

@ -1,5 +1,5 @@
declare module 'koa-slow' { declare module "koa-slow" {
import { Middleware } from 'koa'; import type { Middleware } from "koa";
interface ISlowOptions { interface ISlowOptions {
url?: RegExp; url?: RegExp;

View File

@ -1,4 +1,4 @@
declare module 'os-utils' { declare module "os-utils" {
type FreeCommandCallback = (usedmem: number) => void; type FreeCommandCallback = (usedmem: number) => void;
type HarddriveCallback = (total: number, free: number, used: number) => void; type HarddriveCallback = (total: number, free: number, used: number) => void;
@ -20,7 +20,10 @@ declare module 'os-utils' {
export function harddrive(callback: HarddriveCallback): void; export function harddrive(callback: HarddriveCallback): void;
export function getProcesses(callback: GetProcessesCallback): void; export function getProcesses(callback: GetProcessesCallback): void;
export function getProcesses(nProcess: number, callback: GetProcessesCallback): void; export function getProcesses(
nProcess: number,
callback: GetProcessesCallback,
): void;
export function allLoadavg(): string; export function allLoadavg(): string;
export function loadavg(_time?: number): number; export function loadavg(_time?: number): number;

View File

@ -1,4 +1,4 @@
declare module '*/package.json' { declare module "*/package.json" {
interface IRepository { interface IRepository {
type: string; type: string;
url: string; url: string;

View File

@ -1,5 +1,5 @@
declare module 'probe-image-size' { declare module "probe-image-size" {
import { ReadStream } from 'node:fs'; import type { ReadStream } from "node:fs";
type ProbeOptions = { type ProbeOptions = {
retries: 1; retries: 1;
@ -12,14 +12,24 @@ declare module 'probe-image-size' {
length?: number; length?: number;
type: string; type: string;
mime: string; mime: string;
wUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex'; wUnits: "in" | "mm" | "cm" | "pt" | "pc" | "px" | "em" | "ex";
hUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex'; hUnits: "in" | "mm" | "cm" | "pt" | "pc" | "px" | "em" | "ex";
url?: string; url?: string;
}; };
function probeImageSize(src: string | ReadStream, options?: ProbeOptions): Promise<ProbeResult>; function probeImageSize(
function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void; src: string | ReadStream,
function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void; options?: ProbeOptions,
): Promise<ProbeResult>;
function probeImageSize(
src: string | ReadStream,
callback: (err: Error | null, result?: ProbeResult) => void,
): void;
function probeImageSize(
src: string | ReadStream,
options: ProbeOptions,
callback: (err: Error | null, result?: ProbeResult) => void,
): void;
namespace probeImageSize {} // Hack namespace probeImageSize {} // Hack

View File

@ -1,79 +1,78 @@
import cluster from 'node:cluster'; import cluster from "node:cluster";
import chalk from 'chalk'; import chalk from "chalk";
import Xev from 'xev'; import Xev from "xev";
import Logger from '@/services/logger.js'; import Logger from "@/services/logger.js";
import { envOption } from '../env.js'; import { envOption } from "../env.js";
// for typeorm // for typeorm
import 'reflect-metadata'; import "reflect-metadata";
import { masterMain } from './master.js'; import { masterMain } from "./master.js";
import { workerMain } from './worker.js'; import { workerMain } from "./worker.js";
const logger = new Logger('core', 'cyan'); const logger = new Logger("core", "cyan");
const clusterLogger = logger.createSubLogger('cluster', 'orange', false); const clusterLogger = logger.createSubLogger("cluster", "orange", false);
const ev = new Xev(); const ev = new Xev();
/** /**
* Init process * Init process
*/ */
export default async function() { export default async function () {
process.title = `Calckey (${cluster.isPrimary ? 'master' : 'worker'})`; process.title = `Calckey (${cluster.isPrimary ? "master" : "worker"})`;
if (cluster.isPrimary || envOption.disableClustering) { if (cluster.isPrimary || envOption.disableClustering) {
await masterMain(); await masterMain();
if (cluster.isPrimary) { if (cluster.isPrimary) {
ev.mount(); ev.mount();
} }
} }
if (cluster.isWorker || envOption.disableClustering) { if (cluster.isWorker || envOption.disableClustering) {
await workerMain(); await workerMain();
} }
// For when Calckey is started in a child process during unit testing. // For when Calckey is started in a child process during unit testing.
// Otherwise, process.send cannot be used, so start it. // Otherwise, process.send cannot be used, so start it.
if (process.send) { if (process.send) {
process.send('ok'); process.send("ok");
} }
} }
//#region Events //#region Events
// Listen new workers // Listen new workers
cluster.on('fork', worker => { cluster.on("fork", (worker) => {
clusterLogger.debug(`Process forked: [${worker.id}]`); clusterLogger.debug(`Process forked: [${worker.id}]`);
}); });
// Listen online workers // Listen online workers
cluster.on('online', worker => { cluster.on("online", (worker) => {
clusterLogger.debug(`Process is now online: [${worker.id}]`); clusterLogger.debug(`Process is now online: [${worker.id}]`);
}); });
// Listen for dying workers // Listen for dying workers
cluster.on('exit', worker => { cluster.on("exit", (worker) => {
// Replace the dead worker, // Replace the dead worker,
// we're not sentimental // we're not sentimental
clusterLogger.error(chalk.red(`[${worker.id}] died :(`)); clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
cluster.fork(); cluster.fork();
}); });
// 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", console.dir);
} }
// 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(err);
} catch { } } catch {}
}); });
// Dying away... // Dying away...
process.on('exit', code => { process.on("exit", (code) => {
logger.info(`The process is going to exit with code ${code}`); logger.info(`The process is going to exit with code ${code}`);
}); });
//#endregion //#endregion

View File

@ -1,50 +1,64 @@
import * as fs from 'node:fs'; import * as fs from "node:fs";
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from "node:url";
import { dirname } from 'node:path'; import { dirname } from "node:path";
import * as os from 'node:os'; import * as os from "node:os";
import cluster from 'node:cluster'; import cluster from "node:cluster";
import chalk from 'chalk'; import chalk from "chalk";
import chalkTemplate from 'chalk-template'; import chalkTemplate from "chalk-template";
import semver from 'semver'; import semver from "semver";
import Logger from '@/services/logger.js'; import Logger from "@/services/logger.js";
import loadConfig from '@/config/load.js'; import loadConfig from "@/config/load.js";
import { Config } from '@/config/types.js'; import type { Config } from "@/config/types.js";
import { lessThan } from '@/prelude/array.js'; import { lessThan } from "@/prelude/array.js";
import { envOption } from '../env.js'; import { envOption } from "../env.js";
import { showMachineInfo } from '@/misc/show-machine-info.js'; import { showMachineInfo } from "@/misc/show-machine-info.js";
import { db, initDb } from '../db/postgre.js'; import { db, initDb } from "../db/postgre.js";
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const meta = JSON.parse(
fs.readFileSync(`${_dirname}/../../../../built/meta.json`, "utf-8"),
);
const logger = new Logger('core', 'cyan'); const logger = new Logger("core", "cyan");
const bootLogger = logger.createSubLogger('boot', 'magenta', false); const bootLogger = logger.createSubLogger("boot", "magenta", false);
const themeColor = chalk.hex('#31748f'); const themeColor = chalk.hex("#31748f");
function greet() { function greet() {
if (!envOption.quiet) { if (!envOption.quiet) {
//#region Calckey logo //#region Calckey logo
const v = `v${meta.version}`; const v = `v${meta.version}`;
console.log(themeColor(' ___ _ _ ')); console.log(themeColor(" ___ _ _ "));
console.log(themeColor(' / __\\__ _| | ___| | _____ _ _ ')); console.log(themeColor(" / __\\__ _| | ___| | _____ _ _ "));
console.log(themeColor(' / / / _` | |/ __| |/ / _ \ | | |')); console.log(themeColor(" / / / _` | |/ __| |/ / _ | | |"));
console.log(themeColor('/ /__| (_| | | (__| < __/ |_| |')); console.log(themeColor("/ /__| (_| | | (__| < __/ |_| |"));
console.log(themeColor('\\____/\\__,_|_|\\___|_|\\_\\___|\\__, |')); console.log(themeColor("\\____/\\__,_|_|\\___|_|\\_\\___|\\__, |"));
console.log(themeColor(' (___/ ')); console.log(themeColor(" (___/ "));
//#endregion //#endregion
console.log(' Calckey is an open-source decentralized microblogging platform.'); console.log(
console.log(chalk.rgb(255, 136, 0)(' If you like Calckey, please consider starring or contributing to the repo. https://codeberg.org/calckey/calckey')); " Calckey is an open-source decentralized microblogging platform.",
);
console.log(
chalk.rgb(
255,
136,
0,
)(
" If you like Calckey, please consider starring or contributing to the repo. https://codeberg.org/calckey/calckey",
),
);
console.log(''); console.log("");
console.log(chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`); console.log(
chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`,
);
} }
bootLogger.info('Welcome to Calckey!'); bootLogger.info("Welcome to Calckey!");
bootLogger.info(`Calckey v${meta.version}`, null, true); bootLogger.info(`Calckey v${meta.version}`, null, true);
} }
@ -63,42 +77,50 @@ export async function masterMain() {
config = loadConfigBoot(); config = loadConfigBoot();
await connectDb(); await connectDb();
} catch (e) { } catch (e) {
bootLogger.error('Fatal error occurred during initialization', null, true); bootLogger.error("Fatal error occurred during initialization", null, true);
process.exit(1); process.exit(1);
} }
bootLogger.succ('Calckey initialized'); bootLogger.succ("Calckey initialized");
if (!envOption.disableClustering) { if (!envOption.disableClustering) {
await spawnWorkers(config.clusterLimit); await spawnWorkers(config.clusterLimit);
} }
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); bootLogger.succ(
`Now listening on port ${config.port} on ${config.url}`,
null,
true,
);
if (!envOption.noDaemons) { if (!envOption.noDaemons) {
import('../daemons/server-stats.js').then(x => x.default()); import("../daemons/server-stats.js").then((x) => x.default());
import('../daemons/queue-stats.js').then(x => x.default()); import("../daemons/queue-stats.js").then((x) => x.default());
import('../daemons/janitor.js').then(x => x.default()); import("../daemons/janitor.js").then((x) => x.default());
} }
} }
function showEnvironment(): void { function showEnvironment(): void {
const env = process.env.NODE_ENV; const env = process.env.NODE_ENV;
const logger = bootLogger.createSubLogger('env'); const logger = bootLogger.createSubLogger("env");
logger.info(typeof env === 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`); logger.info(
typeof env === "undefined" ? "NODE_ENV is not set" : `NODE_ENV: ${env}`,
);
if (env !== 'production') { if (env !== "production") {
logger.warn('The environment is not in production mode.'); logger.warn("The environment is not in production mode.");
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true); logger.warn("DO NOT USE FOR PRODUCTION PURPOSE!", null, true);
} }
} }
function showNodejsVersion(): void { function showNodejsVersion(): void {
const nodejsLogger = bootLogger.createSubLogger('nodejs'); const nodejsLogger = bootLogger.createSubLogger("nodejs");
nodejsLogger.info(`Version ${process.version} detected.`); nodejsLogger.info(`Version ${process.version} detected.`);
const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim(); const minVersion = fs
.readFileSync(`${_dirname}/../../../../.node-version`, "utf-8")
.trim();
if (semver.lt(process.version, minVersion)) { if (semver.lt(process.version, minVersion)) {
nodejsLogger.error(`At least Node.js ${minVersion} required!`); nodejsLogger.error(`At least Node.js ${minVersion} required!`);
process.exit(1); process.exit(1);
@ -106,14 +128,14 @@ function showNodejsVersion(): void {
} }
function loadConfigBoot(): Config { function loadConfigBoot(): Config {
const configLogger = bootLogger.createSubLogger('config'); const configLogger = bootLogger.createSubLogger("config");
let config; let config;
try { try {
config = loadConfig(); config = loadConfig();
} catch (exception) { } catch (exception) {
if (exception.code === 'ENOENT') { if (exception.code === "ENOENT") {
configLogger.error('Configuration file not found', null, true); configLogger.error("Configuration file not found", null, true);
process.exit(1); process.exit(1);
} else if (e instanceof Error) { } else if (e instanceof Error) {
configLogger.error(e.message); configLogger.error(e.message);
@ -122,22 +144,24 @@ function loadConfigBoot(): Config {
throw exception; throw exception;
} }
configLogger.succ('Loaded'); configLogger.succ("Loaded");
return config; return config;
} }
async function connectDb(): Promise<void> { async function connectDb(): Promise<void> {
const dbLogger = bootLogger.createSubLogger('db'); const dbLogger = bootLogger.createSubLogger("db");
// Try to connect to DB // Try to connect to DB
try { try {
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.succ(`Connected: v${v}`);
} catch (e) { } catch (e) {
dbLogger.error('Cannot connect', null, true); dbLogger.error("Cannot connect", null, true);
dbLogger.error(e); dbLogger.error(e);
process.exit(1); process.exit(1);
} }
@ -145,20 +169,20 @@ async function connectDb(): Promise<void> {
async function spawnWorkers(limit: number = 1) { async function spawnWorkers(limit: number = 1) {
const workers = Math.min(limit, os.cpus().length); const workers = Math.min(limit, os.cpus().length);
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.succ("All workers started");
} }
function spawnWorker(): Promise<void> { function spawnWorker(): Promise<void> {
return new Promise(res => { return new Promise((res) => {
const worker = cluster.fork(); const worker = cluster.fork();
worker.on('message', message => { worker.on("message", (message) => {
if (message === 'listenFailed') { if (message === "listenFailed") {
bootLogger.error(`The server Listen failed due to the previous error.`); bootLogger.error("The server Listen failed due to the previous error.");
process.exit(1); process.exit(1);
} }
if (message !== 'ready') return; if (message !== "ready") return;
res(); res();
}); });
}); });

View File

@ -1,5 +1,5 @@
import cluster from 'node:cluster'; import cluster from "node:cluster";
import { initDb } from '../db/postgre.js'; import { initDb } from "../db/postgre.js";
/** /**
* Init worker process * Init worker process
@ -8,13 +8,13 @@ export async function workerMain() {
await initDb(); await initDb();
// start server // start server
await import('../server/index.js').then(x => x.default()); await import("../server/index.js").then((x) => x.default());
// start job queue // start job queue
import('../queue/index.js').then(x => x.default()); import("../queue/index.js").then((x) => x.default());
if (cluster.isWorker) { if (cluster.isWorker) {
// Send a 'ready' message to parent process // Send a 'ready' message to parent process
process.send!('ready'); process.send!("ready");
} }
} }

View File

@ -1,3 +1,3 @@
import load from './load.js'; import load from "./load.js";
export default load(); export default load();

View File

@ -2,11 +2,11 @@
* Config loader * Config loader
*/ */
import * as fs from 'node:fs'; import * as fs from "node:fs";
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from "node:url";
import { dirname } from 'node:path'; import { dirname } from "node:path";
import * as yaml from 'js-yaml'; import * as yaml from "js-yaml";
import type { Source, Mixin } from './types.js'; import type { Source, Mixin } from "./types.js";
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);
@ -19,14 +19,20 @@ const dir = `${_dirname}/../../../../.config`;
/** /**
* Path of configuration file * Path of configuration file
*/ */
const path = process.env.NODE_ENV === 'test' const path =
? `${dir}/test.yml` process.env.NODE_ENV === "test" ? `${dir}/test.yml` : `${dir}/default.yml`;
: `${dir}/default.yml`;
export default function load() { export default function load() {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const meta = JSON.parse(
const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8')); fs.readFileSync(`${_dirname}/../../../../built/meta.json`, "utf-8"),
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; );
const clientManifest = JSON.parse(
fs.readFileSync(
`${_dirname}/../../../../built/_client_dist_/manifest.json`,
"utf-8",
),
);
const config = yaml.load(fs.readFileSync(path, "utf-8")) as Source;
const mixin = {} as Mixin; const mixin = {} as Mixin;
@ -34,19 +40,19 @@ export default function load() {
config.url = url.origin; config.url = url.origin;
config.port = config.port || parseInt(process.env.PORT || '', 10); config.port = config.port || parseInt(process.env.PORT || "", 10);
mixin.version = meta.version; mixin.version = meta.version;
mixin.host = url.host; mixin.host = url.host;
mixin.hostname = url.hostname; mixin.hostname = url.hostname;
mixin.scheme = url.protocol.replace(/:$/, ''); mixin.scheme = url.protocol.replace(/:$/, "");
mixin.wsScheme = mixin.scheme.replace('http', 'ws'); mixin.wsScheme = mixin.scheme.replace("http", "ws");
mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`; mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`;
mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`; mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`;
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
mixin.userAgent = `Calckey/${meta.version} (${config.url})`; mixin.userAgent = `Calckey/${meta.version} (${config.url})`;
mixin.clientEntry = clientManifest['src/init.ts']; mixin.clientEntry = clientManifest["src/init.ts"];
if (!config.redis.prefix) config.redis.prefix = mixin.host; if (!config.redis.prefix) config.redis.prefix = mixin.host;

View File

@ -47,7 +47,7 @@ export type Source = {
id: string; id: string;
outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual'; outgoingAddressFamily?: "ipv4" | "ipv6" | "dual";
deliverJobConcurrency?: number; deliverJobConcurrency?: number;
inboxJobConcurrency?: number; inboxJobConcurrency?: number;
@ -81,7 +81,6 @@ export type Source = {
user?: string; user?: string;
pass?: string; pass?: string;
useImplicitSslTls?: boolean; useImplicitSslTls?: boolean;
}; };
objectStorage: { objectStorage: {
managed?: boolean; managed?: boolean;

View File

@ -1,6 +1,7 @@
import config from '@/config/index.js'; import config from "@/config/index.js";
export const MAX_NOTE_TEXT_LENGTH = config.maxNoteLength != null ? config.maxNoteLength : 3000; export const MAX_NOTE_TEXT_LENGTH =
config.maxNoteLength != null ? config.maxNoteLength : 3000;
export const SECOND = 1000; export const SECOND = 1000;
export const SEC = 1000; export const SEC = 1000;
@ -17,39 +18,39 @@ export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
// SVGはXSSを生むので許可しない // SVGはXSSを生むので許可しない
export const FILE_TYPE_BROWSERSAFE = [ export const FILE_TYPE_BROWSERSAFE = [
// Images // Images
'image/png', "image/png",
'image/gif', "image/gif",
'image/jpeg', "image/jpeg",
'image/webp', "image/webp",
'image/apng', "image/apng",
'image/bmp', "image/bmp",
'image/tiff', "image/tiff",
'image/x-icon', "image/x-icon",
// OggS // OggS
'audio/opus', "audio/opus",
'video/ogg', "video/ogg",
'audio/ogg', "audio/ogg",
'application/ogg', "application/ogg",
// ISO/IEC base media file format // ISO/IEC base media file format
'video/quicktime', "video/quicktime",
'video/mp4', "video/mp4",
'audio/mp4', "audio/mp4",
'video/x-m4v', "video/x-m4v",
'audio/x-m4a', "audio/x-m4a",
'video/3gpp', "video/3gpp",
'video/3gpp2', "video/3gpp2",
'video/mpeg', "video/mpeg",
'audio/mpeg', "audio/mpeg",
'video/webm', "video/webm",
'audio/webm', "audio/webm",
'audio/aac', "audio/aac",
'audio/x-flac', "audio/x-flac",
'audio/vnd.wave', "audio/vnd.wave",
]; ];
/* /*
https://github.com/sindresorhus/file-type/blob/main/supported.js https://github.com/sindresorhus/file-type/blob/main/supported.js

View File

@ -1,13 +1,13 @@
// TODO: 消したい // TODO: 消したい
const interval = 30 * 60 * 1000; const interval = 30 * 60 * 1000;
import { AttestationChallenges } from '@/models/index.js'; import { AttestationChallenges } from "@/models/index.js";
import { LessThan } from 'typeorm'; import { LessThan } from "typeorm";
/** /**
* Clean up database occasionally * Clean up database occasionally
*/ */
export default function() { export default function () {
async function tick() { async function tick() {
await AttestationChallenges.delete({ await AttestationChallenges.delete({
createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)), createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)),

View File

@ -1,5 +1,5 @@
import Xev from 'xev'; import Xev from "xev";
import { deliverQueue, inboxQueue } from '../queue/queues.js'; import { deliverQueue, inboxQueue } from "../queue/queues.js";
const ev = new Xev(); const ev = new Xev();
@ -8,21 +8,21 @@ const interval = 10000;
/** /**
* Report queue stats regularly * Report queue stats regularly
*/ */
export default function() { export default function () {
const log = [] as any[]; const log = [] as any[];
ev.on('requestQueueStatsLog', x => { ev.on("requestQueueStatsLog", (x) => {
ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50)); ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50));
}); });
let activeDeliverJobs = 0; let activeDeliverJobs = 0;
let activeInboxJobs = 0; let activeInboxJobs = 0;
deliverQueue.on('global:active', () => { deliverQueue.on("global:active", () => {
activeDeliverJobs++; activeDeliverJobs++;
}); });
inboxQueue.on('global:active', () => { inboxQueue.on("global:active", () => {
activeInboxJobs++; activeInboxJobs++;
}); });
@ -45,7 +45,7 @@ export default function() {
}, },
}; };
ev.emit('queueStats', stats); ev.emit("queueStats", stats);
log.unshift(stats); log.unshift(stats);
if (log.length > 200) log.pop(); if (log.length > 200) log.pop();

View File

@ -1,6 +1,6 @@
import si from 'systeminformation'; import si from "systeminformation";
import Xev from 'xev'; import Xev from "xev";
import * as osUtils from 'os-utils'; import * as osUtils from "os-utils";
const ev = new Xev(); const ev = new Xev();
@ -12,10 +12,10 @@ const round = (num: number) => Math.round(num * 10) / 10;
/** /**
* Report server stats regularly * Report server stats regularly
*/ */
export default function() { export default function () {
const log = [] as any[]; const log = [] as any[];
ev.on('requestServerStatsLog', x => { ev.on("requestServerStatsLog", (x) => {
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50)); ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
}); });
@ -40,7 +40,7 @@ export default function() {
w: round(Math.max(0, fsStats.wIO_sec ?? 0)), w: round(Math.max(0, fsStats.wIO_sec ?? 0)),
}, },
}; };
ev.emit('serverStats', stats); ev.emit("serverStats", stats);
log.unshift(stats); log.unshift(stats);
if (log.length > 200) log.pop(); if (log.length > 200) log.pop();
} }

View File

@ -1,12 +1,12 @@
import * as elasticsearch from '@elastic/elasticsearch'; import * as elasticsearch from "@elastic/elasticsearch";
import config from '@/config/index.js'; import config from "@/config/index.js";
const index = { const index = {
settings: { settings: {
analysis: { analysis: {
analyzer: { analyzer: {
ngram: { ngram: {
tokenizer: 'ngram', tokenizer: "ngram",
}, },
}, },
}, },
@ -14,16 +14,16 @@ const index = {
mappings: { mappings: {
properties: { properties: {
text: { text: {
type: 'text', type: "text",
index: true, index: true,
analyzer: 'ngram', analyzer: "ngram",
}, },
userId: { userId: {
type: 'keyword', type: "keyword",
index: true, index: true,
}, },
userHost: { userHost: {
type: 'keyword', type: "keyword",
index: true, index: true,
}, },
}, },
@ -31,26 +31,35 @@ const index = {
}; };
// Init ElasticSearch connection // Init ElasticSearch connection
const client = config.elasticsearch ? new elasticsearch.Client({ const client = config.elasticsearch
node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`, ? new elasticsearch.Client({
auth: (config.elasticsearch.user && config.elasticsearch.pass) ? { node: `${config.elasticsearch.ssl ? "https://" : "http://"}${
username: config.elasticsearch.user, config.elasticsearch.host
password: config.elasticsearch.pass, }:${config.elasticsearch.port}`,
} : undefined, auth:
pingTimeout: 30000, config.elasticsearch.user && config.elasticsearch.pass
}) : null; ? {
username: config.elasticsearch.user,
password: config.elasticsearch.pass,
}
: undefined,
pingTimeout: 30000,
})
: null;
if (client) { if (client) {
client.indices.exists({ client.indices
index: config.elasticsearch.index || 'misskey_note', .exists({
}).then(exist => { index: config.elasticsearch.index || "misskey_note",
if (!exist.body) { })
client.indices.create({ .then((exist) => {
index: config.elasticsearch.index || 'misskey_note', if (!exist.body) {
body: index, client.indices.create({
}); index: config.elasticsearch.index || "misskey_note",
} body: index,
}); });
}
});
} }
export default client; export default client;

View File

@ -1,3 +1,3 @@
import Logger from '@/services/logger.js'; import Logger from "@/services/logger.js";
export const dbLogger = new Logger('db'); export const dbLogger = new Logger("db");

View File

@ -1,87 +1,89 @@
// https://github.com/typeorm/typeorm/issues/2400 // https://github.com/typeorm/typeorm/issues/2400
import pg from 'pg'; import pg from "pg";
pg.types.setTypeParser(20, Number); pg.types.setTypeParser(20, Number);
import { Logger, DataSource } from 'typeorm'; import type { Logger } from "typeorm";
import * as highlight from 'cli-highlight'; import { DataSource } from "typeorm";
import config from '@/config/index.js'; import * as highlight from "cli-highlight";
import config from "@/config/index.js";
import { User } from '@/models/entities/user.js'; import { User } from "@/models/entities/user.js";
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from "@/models/entities/drive-file.js";
import { DriveFolder } from '@/models/entities/drive-folder.js'; import { DriveFolder } from "@/models/entities/drive-folder.js";
import { AccessToken } from '@/models/entities/access-token.js'; import { AccessToken } from "@/models/entities/access-token.js";
import { App } from '@/models/entities/app.js'; import { App } from "@/models/entities/app.js";
import { PollVote } from '@/models/entities/poll-vote.js'; import { PollVote } from "@/models/entities/poll-vote.js";
import { Note } from '@/models/entities/note.js'; import { Note } from "@/models/entities/note.js";
import { NoteReaction } from '@/models/entities/note-reaction.js'; import { NoteReaction } from "@/models/entities/note-reaction.js";
import { NoteWatching } from '@/models/entities/note-watching.js'; import { NoteWatching } from "@/models/entities/note-watching.js";
import { NoteThreadMuting } from '@/models/entities/note-thread-muting.js'; import { NoteThreadMuting } from "@/models/entities/note-thread-muting.js";
import { NoteUnread } from '@/models/entities/note-unread.js'; import { NoteUnread } from "@/models/entities/note-unread.js";
import { Notification } from '@/models/entities/notification.js'; import { Notification } from "@/models/entities/notification.js";
import { Meta } from '@/models/entities/meta.js'; import { Meta } from "@/models/entities/meta.js";
import { Following } from '@/models/entities/following.js'; import { Following } from "@/models/entities/following.js";
import { Instance } from '@/models/entities/instance.js'; import { Instance } from "@/models/entities/instance.js";
import { Muting } from '@/models/entities/muting.js'; import { Muting } from "@/models/entities/muting.js";
import { SwSubscription } from '@/models/entities/sw-subscription.js'; import { SwSubscription } from "@/models/entities/sw-subscription.js";
import { Blocking } from '@/models/entities/blocking.js'; import { Blocking } from "@/models/entities/blocking.js";
import { UserList } from '@/models/entities/user-list.js'; import { UserList } from "@/models/entities/user-list.js";
import { UserListJoining } from '@/models/entities/user-list-joining.js'; import { UserListJoining } from "@/models/entities/user-list-joining.js";
import { UserGroup } from '@/models/entities/user-group.js'; import { UserGroup } from "@/models/entities/user-group.js";
import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; import { UserGroupJoining } from "@/models/entities/user-group-joining.js";
import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; import { UserGroupInvitation } from "@/models/entities/user-group-invitation.js";
import { Hashtag } from '@/models/entities/hashtag.js'; import { Hashtag } from "@/models/entities/hashtag.js";
import { NoteFavorite } from '@/models/entities/note-favorite.js'; import { NoteFavorite } from "@/models/entities/note-favorite.js";
import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; import { AbuseUserReport } from "@/models/entities/abuse-user-report.js";
import { RegistrationTicket } from '@/models/entities/registration-tickets.js'; import { RegistrationTicket } from "@/models/entities/registration-tickets.js";
import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessage } from "@/models/entities/messaging-message.js";
import { Signin } from '@/models/entities/signin.js'; import { Signin } from "@/models/entities/signin.js";
import { AuthSession } from '@/models/entities/auth-session.js'; import { AuthSession } from "@/models/entities/auth-session.js";
import { FollowRequest } from '@/models/entities/follow-request.js'; import { FollowRequest } from "@/models/entities/follow-request.js";
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from "@/models/entities/emoji.js";
import { UserNotePining } from '@/models/entities/user-note-pining.js'; import { UserNotePining } from "@/models/entities/user-note-pining.js";
import { Poll } from '@/models/entities/poll.js'; import { Poll } from "@/models/entities/poll.js";
import { UserKeypair } from '@/models/entities/user-keypair.js'; import { UserKeypair } from "@/models/entities/user-keypair.js";
import { UserPublickey } from '@/models/entities/user-publickey.js'; import { UserPublickey } from "@/models/entities/user-publickey.js";
import { UserProfile } from '@/models/entities/user-profile.js'; import { UserProfile } from "@/models/entities/user-profile.js";
import { UserSecurityKey } from '@/models/entities/user-security-key.js'; import { UserSecurityKey } from "@/models/entities/user-security-key.js";
import { AttestationChallenge } from '@/models/entities/attestation-challenge.js'; import { AttestationChallenge } from "@/models/entities/attestation-challenge.js";
import { Page } from '@/models/entities/page.js'; import { Page } from "@/models/entities/page.js";
import { PageLike } from '@/models/entities/page-like.js'; import { PageLike } from "@/models/entities/page-like.js";
import { GalleryPost } from '@/models/entities/gallery-post.js'; import { GalleryPost } from "@/models/entities/gallery-post.js";
import { GalleryLike } from '@/models/entities/gallery-like.js'; import { GalleryLike } from "@/models/entities/gallery-like.js";
import { ModerationLog } from '@/models/entities/moderation-log.js'; import { ModerationLog } from "@/models/entities/moderation-log.js";
import { UsedUsername } from '@/models/entities/used-username.js'; import { UsedUsername } from "@/models/entities/used-username.js";
import { Announcement } from '@/models/entities/announcement.js'; import { Announcement } from "@/models/entities/announcement.js";
import { AnnouncementRead } from '@/models/entities/announcement-read.js'; import { AnnouncementRead } from "@/models/entities/announcement-read.js";
import { Clip } from '@/models/entities/clip.js'; import { Clip } from "@/models/entities/clip.js";
import { ClipNote } from '@/models/entities/clip-note.js'; import { ClipNote } from "@/models/entities/clip-note.js";
import { Antenna } from '@/models/entities/antenna.js'; import { Antenna } from "@/models/entities/antenna.js";
import { AntennaNote } from '@/models/entities/antenna-note.js'; import { AntennaNote } from "@/models/entities/antenna-note.js";
import { PromoNote } from '@/models/entities/promo-note.js'; import { PromoNote } from "@/models/entities/promo-note.js";
import { PromoRead } from '@/models/entities/promo-read.js'; import { PromoRead } from "@/models/entities/promo-read.js";
import { Relay } from '@/models/entities/relay.js'; import { Relay } from "@/models/entities/relay.js";
import { MutedNote } from '@/models/entities/muted-note.js'; import { MutedNote } from "@/models/entities/muted-note.js";
import { Channel } from '@/models/entities/channel.js'; import { Channel } from "@/models/entities/channel.js";
import { ChannelFollowing } from '@/models/entities/channel-following.js'; import { ChannelFollowing } from "@/models/entities/channel-following.js";
import { ChannelNotePining } from '@/models/entities/channel-note-pining.js'; import { ChannelNotePining } from "@/models/entities/channel-note-pining.js";
import { RegistryItem } from '@/models/entities/registry-item.js'; import { RegistryItem } from "@/models/entities/registry-item.js";
import { Ad } from '@/models/entities/ad.js'; import { Ad } from "@/models/entities/ad.js";
import { PasswordResetRequest } from '@/models/entities/password-reset-request.js'; import { PasswordResetRequest } from "@/models/entities/password-reset-request.js";
import { UserPending } from '@/models/entities/user-pending.js'; import { UserPending } from "@/models/entities/user-pending.js";
import { Webhook } from '@/models/entities/webhook.js'; import { Webhook } from "@/models/entities/webhook.js";
import { UserIp } from '@/models/entities/user-ip.js'; import { UserIp } from "@/models/entities/user-ip.js";
import { entities as charts } from '@/services/chart/entities.js'; import { entities as charts } from "@/services/chart/entities.js";
import { envOption } from '../env.js'; import { envOption } from "../env.js";
import { dbLogger } from './logger.js'; import { dbLogger } from "./logger.js";
import { redisClient } from './redis.js'; import { redisClient } from "./redis.js";
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); const sqlLogger = dbLogger.createSubLogger("sql", "gray", false);
class MyCustomLogger implements Logger { class MyCustomLogger implements Logger {
private highlight(sql: string) { private highlight(sql: string) {
return highlight.highlight(sql, { return highlight.highlight(sql, {
language: 'sql', ignoreIllegals: true, language: "sql",
ignoreIllegals: true,
}); });
} }
@ -178,10 +180,10 @@ export const entities = [
...charts, ...charts,
]; ];
const log = process.env.NODE_ENV !== 'production'; const log = process.env.NODE_ENV !== "production";
export const db = new DataSource({ export const db = new DataSource({
type: 'postgres', type: "postgres",
host: config.db.host, host: config.db.host,
port: config.db.port, port: config.db.port,
username: config.db.user, username: config.db.user,
@ -191,24 +193,26 @@ export const db = new DataSource({
statement_timeout: 1000 * 10, statement_timeout: 1000 * 10,
...config.db.extra, ...config.db.extra,
}, },
synchronize: process.env.NODE_ENV === 'test', synchronize: process.env.NODE_ENV === "test",
dropSchema: process.env.NODE_ENV === 'test', dropSchema: process.env.NODE_ENV === "test",
cache: !config.db.disableCache ? { cache: !config.db.disableCache
type: 'ioredis', ? {
options: { type: "ioredis",
host: config.redis.host, options: {
port: config.redis.port, host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family, port: config.redis.port,
password: config.redis.pass, family: config.redis.family == null ? 0 : config.redis.family,
keyPrefix: `${config.redis.prefix}:query:`, password: config.redis.pass,
db: config.redis.db || 0, keyPrefix: `${config.redis.prefix}:query:`,
}, db: config.redis.db || 0,
} : false, },
}
: false,
logging: log, logging: log,
logger: log ? new MyCustomLogger() : undefined, logger: log ? new MyCustomLogger() : undefined,
maxQueryExecutionTime: 300, maxQueryExecutionTime: 300,
entities: entities, entities: entities,
migrations: ['../../migration/*.js'], migrations: ["../../migration/*.js"],
}); });
export async function initDb(force = false) { export async function initDb(force = false) {
@ -247,7 +251,7 @@ export async function resetDb() {
if (i === 3) { if (i === 3) {
throw e; throw e;
} else { } else {
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
continue; continue;
} }
} }

View File

@ -1,5 +1,5 @@
import Redis from 'ioredis'; import Redis from "ioredis";
import config from '@/config/index.js'; import config from "@/config/index.js";
export function createConnection() { export function createConnection() {
return new Redis({ return new Redis({

View File

@ -10,11 +10,16 @@ const envOption = {
}; };
for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) { for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
if (process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]) envOption[key] = true; if (
process.env[
`MK_${key.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase()}`
]
)
envOption[key] = true;
} }
if (process.env.NODE_ENV === 'test') envOption.disableClustering = true; if (process.env.NODE_ENV === "test") envOption.disableClustering = true;
if (process.env.NODE_ENV === 'test') envOption.quiet = true; if (process.env.NODE_ENV === "test") envOption.quiet = true;
if (process.env.NODE_ENV === 'test') envOption.noDaemons = true; if (process.env.NODE_ENV === "test") envOption.noDaemons = true;
export { envOption }; export { envOption };

View File

@ -2,12 +2,12 @@
* Misskey Entry Point! * Misskey Entry Point!
*/ */
import { EventEmitter } from 'node:events'; import { EventEmitter } from "node:events";
import boot from './boot/index.js'; import boot from "./boot/index.js";
Error.stackTraceLimit = Infinity; Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128; EventEmitter.defaultMaxListeners = 128;
boot().catch(err => { boot().catch((err) => {
console.error(err); console.error(err);
}); });

View File

@ -1,6 +1,6 @@
import { URL } from 'node:url'; import { URL } from "node:url";
import * as parse5 from 'parse5'; import * as parse5 from "parse5";
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; import * as TreeAdapter from "../../node_modules/parse5/dist/tree-adapters/default.js";
const treeAdapter = TreeAdapter.defaultTreeAdapter; const treeAdapter = TreeAdapter.defaultTreeAdapter;
@ -9,11 +9,11 @@ const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
export function fromHtml(html: string, hashtagNames?: string[]): string { export function fromHtml(html: string, hashtagNames?: string[]): string {
// some AP servers like Pixelfed use br tags as well as newlines // some AP servers like Pixelfed use br tags as well as newlines
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n'); html = html.replace(/<br\s?\/?>\r?\n/gi, "\n");
const dom = parse5.parseFragment(html); const dom = parse5.parseFragment(html);
let text = ''; let text = "";
for (const n of dom.childNodes) { for (const n of dom.childNodes) {
analyze(n); analyze(n);
@ -23,14 +23,14 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
function getText(node: TreeAdapter.Node): string { function getText(node: TreeAdapter.Node): string {
if (treeAdapter.isTextNode(node)) return node.value; if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return ''; if (!treeAdapter.isElementNode(node)) return "";
if (node.nodeName === 'br') return '\n'; if (node.nodeName === "br") return "\n";
if (node.childNodes) { if (node.childNodes) {
return node.childNodes.map(n => getText(n)).join(''); return node.childNodes.map((n) => getText(n)).join("");
} }
return ''; return "";
} }
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
@ -51,42 +51,46 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
if (!treeAdapter.isElementNode(node)) return; if (!treeAdapter.isElementNode(node)) return;
switch (node.nodeName) { switch (node.nodeName) {
case 'br': { case "br": {
text += '\n'; text += "\n";
break; break;
} }
case 'a': case "a": {
{
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel'); const rel = node.attrs.find((x) => x.name === "rel");
const href = node.attrs.find(x => x.name === 'href'); const href = node.attrs.find((x) => x.name === "href");
// ハッシュタグ // ハッシュタグ
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { if (
hashtagNames &&
href &&
hashtagNames.map((x) => x.toLowerCase()).includes(txt.toLowerCase())
) {
text += txt; text += txt;
// メンション // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { } else if (txt.startsWith("@") && !(rel?.value.match(/^me /))) {
const part = txt.split('@'); const part = txt.split("@");
if (part.length === 2 && href) { if (part.length === 2 && href) {
//#region ホスト名部分が省略されているので復元する //#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`; const acct = `${txt}@${new URL(href.value).hostname}`;
text += acct; text += acct;
//#endregion //#endregion
} else if (part.length === 3) { } else if (part.length === 3) {
text += txt; text += txt;
} }
// その他 // その他
} else { } else {
const generateLink = () => { const generateLink = () => {
if (!href && !txt) { if (!(href || txt)) {
return ''; return "";
} }
if (!href) { if (!href) {
return txt; return txt;
} }
if (!txt || txt === href.value) { // #6383: Missing text node if (!txt || txt === href.value) {
// #6383: Missing text node
if (href.value.match(urlRegexFull)) { if (href.value.match(urlRegexFull)) {
return href.value; return href.value;
} else { } else {
@ -94,7 +98,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
} }
} }
if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) { if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) {
return `[${txt}](<${href.value}>)`; // #6846 return `[${txt}](<${href.value}>)`; // #6846
} else { } else {
return `[${txt}](${href.value})`; return `[${txt}](${href.value})`;
} }
@ -105,55 +109,53 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
break; break;
} }
case 'h1': case "h1": {
{ text += "【";
text += '【';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '】\n'; text += "】\n";
break; break;
} }
case 'b': case "b":
case 'strong': case "strong": {
{ text += "**";
text += '**';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '**'; text += "**";
break; break;
} }
case 'small': case "small": {
{ text += "<small>";
text += '<small>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</small>'; text += "</small>";
break; break;
} }
case 's': case "s":
case 'del': case "del": {
{ text += "~~";
text += '~~';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '~~'; text += "~~";
break; break;
} }
case 'i': case "i":
case 'em': case "em": {
{ text += "<i>";
text += '<i>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</i>'; text += "</i>";
break; break;
} }
// block code (<pre><code>) // block code (<pre><code>)
case 'pre': { case "pre": {
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') { if (
text += '\n```\n'; node.childNodes.length === 1 &&
node.childNodes[0].nodeName === "code"
) {
text += "\n```\n";
text += getText(node.childNodes[0]); text += getText(node.childNodes[0]);
text += '\n```\n'; text += "\n```\n";
} else { } else {
appendChildren(node.childNodes); appendChildren(node.childNodes);
} }
@ -161,50 +163,48 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
} }
// inline code (<code>) // inline code (<code>)
case 'code': { case "code": {
text += '`'; text += "`";
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '`'; text += "`";
break; break;
} }
case 'blockquote': { case "blockquote": {
const t = getText(node); const t = getText(node);
if (t) { if (t) {
text += '\n> '; text += "\n> ";
text += t.split('\n').join('\n> '); text += t.split("\n").join("\n> ");
} }
break; break;
} }
case 'p': case "p":
case 'h2': case "h2":
case 'h3': case "h3":
case 'h4': case "h4":
case 'h5': case "h5":
case 'h6': case "h6": {
{ text += "\n\n";
text += '\n\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
// other block elements // other block elements
case 'div': case "div":
case 'header': case "header":
case 'footer': case "footer":
case 'article': case "article":
case 'li': case "li":
case 'dt': case "dt":
case 'dd': case "dd": {
{ text += "\n";
text += '\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
default: // includes inline elements default: {
{ // includes inline elements
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }

View File

@ -1,65 +1,71 @@
import { JSDOM } from 'jsdom'; import { JSDOM } from "jsdom";
import * as mfm from 'mfm-js'; import type * as mfm from "mfm-js";
import config from '@/config/index.js'; import config from "@/config/index.js";
import { intersperse } from '@/prelude/array.js'; import { intersperse } from "@/prelude/array.js";
import { IMentionedRemoteUsers } from '@/models/entities/note.js'; import type { IMentionedRemoteUsers } from "@/models/entities/note.js";
export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { export function toHtml(
nodes: mfm.MfmNode[] | null,
mentionedRemoteUsers: IMentionedRemoteUsers = [],
) {
if (nodes == null) { if (nodes == null) {
return null; return null;
} }
const { window } = new JSDOM(''); const { window } = new JSDOM("");
const doc = window.document; const doc = window.document;
function appendChildren(children: mfm.MfmNode[], targetElement: any): void { function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) { if (children) {
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child); for (const child of children.map((x) => (handlers as any)[x.type](x)))
targetElement.appendChild(child);
} }
} }
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = { const handlers: {
[K in mfm.MfmNode["type"]]: (node: mfm.NodeType<K>) => any;
} = {
bold(node) { bold(node) {
const el = doc.createElement('b'); const el = doc.createElement("b");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
small(node) { small(node) {
const el = doc.createElement('small'); const el = doc.createElement("small");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
strike(node) { strike(node) {
const el = doc.createElement('del'); const el = doc.createElement("del");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
italic(node) { italic(node) {
const el = doc.createElement('i'); const el = doc.createElement("i");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
fn(node) { fn(node) {
const el = doc.createElement('i'); const el = doc.createElement("i");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
blockCode(node) { blockCode(node) {
const pre = doc.createElement('pre'); const pre = doc.createElement("pre");
const inner = doc.createElement('code'); const inner = doc.createElement("code");
inner.textContent = node.props.code; inner.textContent = node.props.code;
pre.appendChild(inner); pre.appendChild(inner);
return pre; return pre;
}, },
center(node) { center(node) {
const el = doc.createElement('div'); const el = doc.createElement("div");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
@ -73,81 +79,90 @@ export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMenti
}, },
hashtag(node) { hashtag(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
a.href = `${config.url}/tags/${node.props.hashtag}`; a.href = `${config.url}/tags/${node.props.hashtag}`;
a.textContent = `#${node.props.hashtag}`; a.textContent = `#${node.props.hashtag}`;
a.setAttribute('rel', 'tag'); a.setAttribute("rel", "tag");
return a; return a;
}, },
inlineCode(node) { inlineCode(node) {
const el = doc.createElement('code'); const el = doc.createElement("code");
el.textContent = node.props.code; el.textContent = node.props.code;
return el; return el;
}, },
mathInline(node) { mathInline(node) {
const el = doc.createElement('code'); const el = doc.createElement("code");
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
mathBlock(node) { mathBlock(node) {
const el = doc.createElement('code'); const el = doc.createElement("code");
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
link(node) { link(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
a.href = node.props.url; a.href = node.props.url;
appendChildren(node.children, a); appendChildren(node.children, a);
return a; return a;
}, },
mention(node) { mention(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); const remoteUserInfo = mentionedRemoteUsers.find(
a.href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${config.url}/${acct}`; (remoteUser) =>
a.className = 'u-url mention'; remoteUser.username === username && remoteUser.host === host,
);
a.href = remoteUserInfo
? remoteUserInfo.url
? remoteUserInfo.url
: remoteUserInfo.uri
: `${config.url}/${acct}`;
a.className = "u-url mention";
a.textContent = acct; a.textContent = acct;
return a; return a;
}, },
quote(node) { quote(node) {
const el = doc.createElement('blockquote'); const el = doc.createElement("blockquote");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
text(node) { text(node) {
const el = doc.createElement('span'); const el = doc.createElement("span");
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x)); const nodes = node.props.text
.split(/\r\n|\r|\n/)
.map((x) => doc.createTextNode(x));
for (const x of intersperse<FIXME | 'br'>('br', nodes)) { for (const x of intersperse<FIXME | "br">("br", nodes)) {
el.appendChild(x === 'br' ? doc.createElement('br') : x); el.appendChild(x === "br" ? doc.createElement("br") : x);
} }
return el; return el;
}, },
url(node) { url(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
a.href = node.props.url; a.href = node.props.url;
a.textContent = node.props.url; a.textContent = node.props.url;
return a; return a;
}, },
search(node) { search(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
a.href = `https://search.annoyingorange.xyz/search?q=${node.props.query}`; a.href = `https://search.annoyingorange.xyz/search?q=${node.props.query}`;
a.textContent = node.props.content; a.textContent = node.props.content;
return a; return a;
}, },
plain(node) { plain(node) {
const el = doc.createElement('span'); const el = doc.createElement("span");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },

View File

@ -4,8 +4,8 @@ export type Acct = {
}; };
export function parse(acct: string): Acct { export function parse(acct: string): Acct {
if (acct.startsWith('@')) acct = acct.substr(1); if (acct.startsWith("@")) acct = acct.substr(1);
const split = acct.split('@', 2); const split = acct.split("@", 2);
return { username: split[0], host: split[1] || null }; return { username: split[0], host: split[1] || null };
} }

View File

@ -1,6 +1,6 @@
import { Antennas } from '@/models/index.js'; import { Antennas } from "@/models/index.js";
import { Antenna } from '@/models/entities/antenna.js'; import type { Antenna } from "@/models/entities/antenna.js";
import { subscriber } from '@/db/redis.js'; import { subscriber } from "@/db/redis.js";
let antennasFetched = false; let antennasFetched = false;
let antennas: Antenna[] = []; let antennas: Antenna[] = [];
@ -14,20 +14,20 @@ export async function getAntennas() {
return antennas; return antennas;
} }
subscriber.on('message', async (_, data) => { subscriber.on("message", async (_, data) => {
const obj = JSON.parse(data); const obj = JSON.parse(data);
if (obj.channel === 'internal') { if (obj.channel === "internal") {
const { type, body } = obj.message; const { type, body } = obj.message;
switch (type) { switch (type) {
case 'antennaCreated': case "antennaCreated":
antennas.push(body); antennas.push(body);
break; break;
case 'antennaUpdated': case "antennaUpdated":
antennas[antennas.findIndex(a => a.id === body.id)] = body; antennas[antennas.findIndex((a) => a.id === body.id)] = body;
break; break;
case 'antennaDeleted': case "antennaDeleted":
antennas = antennas.filter(a => a.id !== body.id); antennas = antennas.filter((a) => a.id !== body.id);
break; break;
default: default:
break; break;

View File

@ -1,35 +1,35 @@
export const kinds = [ export const kinds = [
'read:account', "read:account",
'write:account', "write:account",
'read:blocks', "read:blocks",
'write:blocks', "write:blocks",
'read:drive', "read:drive",
'write:drive', "write:drive",
'read:favorites', "read:favorites",
'write:favorites', "write:favorites",
'read:following', "read:following",
'write:following', "write:following",
'read:messaging', "read:messaging",
'write:messaging', "write:messaging",
'read:mutes', "read:mutes",
'write:mutes', "write:mutes",
'write:notes', "write:notes",
'read:notifications', "read:notifications",
'write:notifications', "write:notifications",
'read:reactions', "read:reactions",
'write:reactions', "write:reactions",
'write:votes', "write:votes",
'read:pages', "read:pages",
'write:pages', "write:pages",
'write:page-likes', "write:page-likes",
'read:page-likes', "read:page-likes",
'read:user-groups', "read:user-groups",
'write:user-groups', "write:user-groups",
'read:channels', "read:channels",
'write:channels', "write:channels",
'read:gallery', "read:gallery",
'write:gallery', "write:gallery",
'read:gallery-likes', "read:gallery-likes",
'write:gallery-likes', "write:gallery-likes",
]; ];
// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions). // IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions).

View File

@ -1,16 +1,15 @@
import { redisClient } from '../db/redis.js'; import { redisClient } from "../db/redis.js";
import { promisify } from 'node:util'; import { promisify } from "node:util";
import redisLock from 'redis-lock'; import redisLock from "redis-lock";
/** /**
* Retry delay (ms) for lock acquisition * Retry delay (ms) for lock acquisition
*/ */
const retryDelay = 100; const retryDelay = 100;
const lock: (key: string, timeout?: number) => Promise<() => void> const lock: (key: string, timeout?: number) => Promise<() => void> = redisClient
= redisClient
? promisify(redisLock(redisClient, retryDelay)) ? promisify(redisLock(redisClient, retryDelay))
: async () => () => { }; : async () => () => {};
/** /**
* Get AP Object lock * Get AP Object lock
@ -22,7 +21,10 @@ export function getApLock(uri: string, timeout = 30 * 1000) {
return lock(`ap-object:${uri}`, timeout); return lock(`ap-object:${uri}`, timeout);
} }
export function getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000) { export function getFetchInstanceMetadataLock(
host: string,
timeout = 30 * 1000,
) {
return lock(`instance:${host}`, timeout); return lock(`instance:${host}`, timeout);
} }

View File

@ -1,6 +1,6 @@
// https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58 // https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58
'use strict'; "use strict";
/** /**
* @callback BeforeShutdownListener * @callback BeforeShutdownListener
@ -11,7 +11,7 @@
* System signals the app will listen to initiate shutdown. * System signals the app will listen to initiate shutdown.
* @const {string[]} * @const {string[]}
*/ */
const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM']; const SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
/** /**
* Time in milliseconds to wait before forcing shutdown. * Time in milliseconds to wait before forcing shutdown.
@ -31,7 +31,10 @@ const shutdownListeners: ((signalOrEvent: string) => void)[] = [];
* @param {string[]} signals System signals to listen to. * @param {string[]} signals System signals to listen to.
* @param {function(string)} fn Function to execute on shutdown. * @param {function(string)} fn Function to execute on shutdown.
*/ */
const processOnce = (signals: string[], fn: (signalOrEvent: string) => void) => { const processOnce = (
signals: string[],
fn: (signalOrEvent: string) => void,
) => {
for (const sig of signals) { for (const sig of signals) {
process.once(sig, fn); process.once(sig, fn);
} }
@ -44,7 +47,9 @@ const processOnce = (signals: string[], fn: (signalOrEvent: string) => void) =>
const forceExitAfter = (timeout: number) => () => { const forceExitAfter = (timeout: number) => () => {
setTimeout(() => { setTimeout(() => {
// Force shutdown after timeout // Force shutdown after timeout
console.warn(`Could not close resources gracefully after ${timeout}ms: forcing shutdown`); console.warn(
`Could not close resources gracefully after ${timeout}ms: forcing shutdown`,
);
return process.exit(1); return process.exit(1);
}, timeout).unref(); }, timeout).unref();
}; };
@ -56,7 +61,7 @@ const forceExitAfter = (timeout: number) => () => {
* @param {string} signalOrEvent The exit signal or event name received on the process. * @param {string} signalOrEvent The exit signal or event name received on the process.
*/ */
async function shutdownHandler(signalOrEvent: string) { async function shutdownHandler(signalOrEvent: string) {
if (process.env.NODE_ENV === 'test') return process.exit(0); if (process.env.NODE_ENV === "test") return process.exit(0);
console.warn(`Shutting down: received [${signalOrEvent}] signal`); console.warn(`Shutting down: received [${signalOrEvent}] signal`);
@ -65,7 +70,11 @@ async function shutdownHandler(signalOrEvent: string) {
await listener(signalOrEvent); await listener(signalOrEvent);
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
console.warn(`A shutdown handler failed before completing with: ${err.message || err}`); console.warn(
`A shutdown handler failed before completing with: ${
err.message || err
}`,
);
} }
} }
} }

View File

@ -1,8 +1,8 @@
export class Cache<T> { export class Cache<T> {
public cache: Map<string | null, { date: number; value: T; }>; public cache: Map<string | null, { date: number; value: T }>;
private lifetime: number; private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) { constructor(lifetime: Cache<never>["lifetime"]) {
this.cache = new Map(); this.cache = new Map();
this.lifetime = lifetime; this.lifetime = lifetime;
} }
@ -17,7 +17,7 @@ export class Cache<T> {
public get(key: string | null): T | undefined { public get(key: string | null): T | undefined {
const cached = this.cache.get(key); const cached = this.cache.get(key);
if (cached == null) return undefined; if (cached == null) return undefined;
if ((Date.now() - cached.date) > this.lifetime) { if (Date.now() - cached.date > this.lifetime) {
this.cache.delete(key); this.cache.delete(key);
return undefined; return undefined;
} }
@ -32,7 +32,11 @@ export class Cache<T> {
* fetcherを呼び出して結果をキャッシュ& * fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/ */
public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> { public async fetch(
key: string | null,
fetcher: () => Promise<T>,
validator?: (cachedValue: T) => boolean,
): Promise<T> {
const cachedValue = this.get(key); const cachedValue = this.get(key);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
if (validator) { if (validator) {
@ -56,7 +60,11 @@ export class Cache<T> {
* fetcherを呼び出して結果をキャッシュ& * fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/ */
public async fetchMaybe(key: string | null, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> { public async fetchMaybe(
key: string | null,
fetcher: () => Promise<T | undefined>,
validator?: (cachedValue: T) => boolean,
): Promise<T | undefined> {
const cachedValue = this.get(key); const cachedValue = this.get(key);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
if (validator) { if (validator) {

View File

@ -1,51 +1,67 @@
import fetch from 'node-fetch'; import fetch from "node-fetch";
import { URLSearchParams } from 'node:url'; import { URLSearchParams } from "node:url";
import { getAgentByUrl } from './fetch.js'; import { getAgentByUrl } from "./fetch.js";
import config from '@/config/index.js'; import config from "@/config/index.js";
export async function verifyRecaptcha(secret: string, response: string) { export async function verifyRecaptcha(secret: string, response: string) {
const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => { const result = await getCaptchaResponse(
"https://www.recaptcha.net/recaptcha/api/siteverify",
secret,
response,
).catch((e) => {
throw new Error(`recaptcha-request-failed: ${e.message}`); throw new Error(`recaptcha-request-failed: ${e.message}`);
}); });
if (result.success !== true) { if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : ''; const errorCodes = result["error-codes"]
? result["error-codes"]?.join(", ")
: "";
throw new Error(`recaptcha-failed: ${errorCodes}`); throw new Error(`recaptcha-failed: ${errorCodes}`);
} }
} }
export async function verifyHcaptcha(secret: string, response: string) { export async function verifyHcaptcha(secret: string, response: string) {
const result = await getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => { const result = await getCaptchaResponse(
"https://hcaptcha.com/siteverify",
secret,
response,
).catch((e) => {
throw new Error(`hcaptcha-request-failed: ${e.message}`); throw new Error(`hcaptcha-request-failed: ${e.message}`);
}); });
if (result.success !== true) { if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : ''; const errorCodes = result["error-codes"]
? result["error-codes"]?.join(", ")
: "";
throw new Error(`hcaptcha-failed: ${errorCodes}`); throw new Error(`hcaptcha-failed: ${errorCodes}`);
} }
} }
type CaptchaResponse = { type CaptchaResponse = {
success: boolean; success: boolean;
'error-codes'?: string[]; "error-codes"?: string[];
}; };
async function getCaptchaResponse(url: string, secret: string, response: string): Promise<CaptchaResponse> { async function getCaptchaResponse(
url: string,
secret: string,
response: string,
): Promise<CaptchaResponse> {
const params = new URLSearchParams({ const params = new URLSearchParams({
secret, secret,
response, response,
}); });
const res = await fetch(url, { const res = await fetch(url, {
method: 'POST', method: "POST",
body: params, body: params,
headers: { headers: {
'User-Agent': config.userAgent, "User-Agent": config.userAgent,
}, },
// TODO // TODO
//timeout: 10 * 1000, //timeout: 10 * 1000,
agent: getAgentByUrl, agent: getAgentByUrl,
}).catch(e => { }).catch((e) => {
throw new Error(`${e.message || e}`); throw new Error(`${e.message || e}`);
}); });
@ -53,5 +69,5 @@ async function getCaptchaResponse(url: string, secret: string, response: string)
throw new Error(`${res.status}`); throw new Error(`${res.status}`);
} }
return await res.json() as CaptchaResponse; return (await res.json()) as CaptchaResponse;
} }

View File

@ -1,90 +1,121 @@
import { Antenna } from '@/models/entities/antenna.js'; import type { Antenna } from "@/models/entities/antenna.js";
import { Note } from '@/models/entities/note.js'; import type { Note } from "@/models/entities/note.js";
import { User } from '@/models/entities/user.js'; import type { User } from "@/models/entities/user.js";
import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js'; import {
import { getFullApAccount } from './convert-host.js'; UserListJoinings,
import * as Acct from '@/misc/acct.js'; UserGroupJoinings,
import { Packed } from './schema.js'; Blockings,
import { Cache } from './cache.js'; } from "@/models/index.js";
import { getFullApAccount } from "./convert-host.js";
import * as Acct from "@/misc/acct.js";
import type { Packed } from "./schema.js";
import { Cache } from "./cache.js";
const blockingCache = new Cache<User['id'][]>(1000 * 60 * 5); const blockingCache = new Cache<User["id"][]>(1000 * 60 * 5);
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
/** /**
* noteUserFollowers / antennaUserFollowing * noteUserFollowers / antennaUserFollowing
*/ */
export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> { export async function checkHitAntenna(
if (note.visibility === 'specified') return false; antenna: Antenna,
note: Note | Packed<"Note">,
noteUser: { id: User["id"]; username: string; host: string | null },
noteUserFollowers?: User["id"][],
antennaUserFollowing?: User["id"][],
): Promise<boolean> {
if (note.visibility === "specified") return false;
// アンテナ作成者がノート作成者にブロックされていたらスキップ // アンテナ作成者がノート作成者にブロックされていたらスキップ
const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); const blockings = await blockingCache.fetch(noteUser.id, () =>
if (blockings.some(blocking => blocking === antenna.userId)) return false; Blockings.findBy({ blockerId: noteUser.id }).then((res) =>
res.map((x) => x.blockeeId),
),
);
if (blockings.some((blocking) => blocking === antenna.userId)) return false;
if (note.visibility === 'followers') { if (note.visibility === "followers") {
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; return false;
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
return false;
} }
if (!antenna.withReplies && note.replyId != null) return false; if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === 'home') { if (antenna.src === "home") {
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; return false;
} else if (antenna.src === 'list') { if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
const listUsers = (await UserListJoinings.findBy({ return false;
userListId: antenna.userListId!, } else if (antenna.src === "list") {
})).map(x => x.userId); const listUsers = (
await UserListJoinings.findBy({
userListId: antenna.userListId!,
})
).map((x) => x.userId);
if (!listUsers.includes(note.userId)) return false; if (!listUsers.includes(note.userId)) return false;
} else if (antenna.src === 'group') { } else if (antenna.src === "group") {
const joining = await UserGroupJoinings.findOneByOrFail({ id: antenna.userGroupJoiningId! }); const joining = await UserGroupJoinings.findOneByOrFail({
id: antenna.userGroupJoiningId!,
});
const groupUsers = (await UserGroupJoinings.findBy({ const groupUsers = (
userGroupId: joining.userGroupId, await UserGroupJoinings.findBy({
})).map(x => x.userId); userGroupId: joining.userGroupId,
})
).map((x) => x.userId);
if (!groupUsers.includes(note.userId)) return false; if (!groupUsers.includes(note.userId)) return false;
} else if (antenna.src === 'users') { } else if (antenna.src === "users") {
const accts = antenna.users.map(x => { const accts = antenna.users.map((x) => {
const { username, host } = Acct.parse(x); const { username, host } = Acct.parse(x);
return getFullApAccount(username, host).toLowerCase(); return getFullApAccount(username, host).toLowerCase();
}); });
if (!accts.includes(getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; if (
!accts.includes(
getFullApAccount(noteUser.username, noteUser.host).toLowerCase(),
)
)
return false;
} }
const keywords = antenna.keywords const keywords = antenna.keywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map((xs) => xs.filter((x) => x !== ""))
.filter(xs => xs.length > 0); .filter((xs) => xs.length > 0);
if (keywords.length > 0) { if (keywords.length > 0) {
if (note.text == null) return false; if (note.text == null) return false;
const matched = keywords.some(and => const matched = keywords.some((and) =>
and.every(keyword => and.every((keyword) =>
antenna.caseSensitive antenna.caseSensitive
? note.text!.includes(keyword) ? note.text!.includes(keyword)
: note.text!.toLowerCase().includes(keyword.toLowerCase()) : note.text!.toLowerCase().includes(keyword.toLowerCase()),
)); ),
);
if (!matched) return false; if (!matched) return false;
} }
const excludeKeywords = antenna.excludeKeywords const excludeKeywords = antenna.excludeKeywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map((xs) => xs.filter((x) => x !== ""))
.filter(xs => xs.length > 0); .filter((xs) => xs.length > 0);
if (excludeKeywords.length > 0) { if (excludeKeywords.length > 0) {
if (note.text == null) return false; if (note.text == null) return false;
const matched = excludeKeywords.some(and => const matched = excludeKeywords.some((and) =>
and.every(keyword => and.every((keyword) =>
antenna.caseSensitive antenna.caseSensitive
? note.text!.includes(keyword) ? note.text!.includes(keyword)
: note.text!.toLowerCase().includes(keyword.toLowerCase()) : note.text!.toLowerCase().includes(keyword.toLowerCase()),
)); ),
);
if (matched) return false; if (matched) return false;
} }

View File

@ -1,28 +1,32 @@
import RE2 from 're2'; import RE2 from "re2";
import { Note } from '@/models/entities/note.js'; import type { Note } from "@/models/entities/note.js";
import { User } from '@/models/entities/user.js'; import type { User } from "@/models/entities/user.js";
type NoteLike = { type NoteLike = {
userId: Note['userId']; userId: Note["userId"];
text: Note['text']; text: Note["text"];
}; };
type UserLike = { type UserLike = {
id: User['id']; id: User["id"];
}; };
export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: Array<string | string[]>): Promise<boolean> { export async function checkWordMute(
note: NoteLike,
me: UserLike | null | undefined,
mutedWords: Array<string | string[]>,
): Promise<boolean> {
// 自分自身 // 自分自身
if (me && (note.userId === me.id)) return false; if (me && note.userId === me.id) return false;
if (mutedWords.length > 0) { if (mutedWords.length > 0) {
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim();
if (text === '') return false; if (text === "") return false;
const matched = mutedWords.some(filter => { const matched = mutedWords.some((filter) => {
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
return filter.every(keyword => text.includes(keyword)); return filter.every((keyword) => text.includes(keyword));
} else { } else {
// represents RegExp // represents RegExp
const regexp = filter.match(/^\/(.+)\/(.*)$/); const regexp = filter.match(/^\/(.+)\/(.*)$/);

View File

@ -1,10 +1,16 @@
// structredCloneが遅いため // structredCloneが遅いため
// SEE: http://var.blog.jp/archives/86038606.html // SEE: http://var.blog.jp/archives/86038606.html
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; type Cloneable =
| string
| number
| boolean
| null
| { [key: string]: Cloneable }
| Cloneable[];
export function deepClone<T extends Cloneable>(x: T): T { export function deepClone<T extends Cloneable>(x: T): T {
if (typeof x === 'object') { if (typeof x === "object") {
if (x === null) return x; if (x === null) return x;
if (Array.isArray(x)) return x.map(deepClone) as T; if (Array.isArray(x)) return x.map(deepClone) as T;
const obj = {} as Record<string, Cloneable>; const obj = {} as Record<string, Cloneable>;

View File

@ -1,6 +1,9 @@
import cd from 'content-disposition'; import cd from "content-disposition";
export function contentDisposition(type: 'inline' | 'attachment', filename: string): string { export function contentDisposition(
const fallback = filename.replace(/[^\w.-]/g, '_'); type: "inline" | "attachment",
filename: string,
): string {
const fallback = filename.replace(/[^\w.-]/g, "_");
return cd(filename, { type, fallback }); return cd(filename, { type, fallback });
} }

View File

@ -1,9 +1,11 @@
import { URL } from 'node:url'; import { URL } from "node:url";
import config from '@/config/index.js'; import config from "@/config/index.js";
import { toASCII } from 'punycode'; import { toASCII } from "punycode";
export function getFullApAccount(username: string, host: string | null) { export function getFullApAccount(username: string, host: string | null) {
return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`; return host
? `${username}@${toPuny(host)}`
: `${username}@${toPuny(config.host)}`;
} }
export function isSelfHost(host: string) { export function isSelfHost(host: string) {

View File

@ -1,14 +1,18 @@
import { Notes } from '@/models/index.js'; import { Notes } from "@/models/index.js";
export async function countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> { export async function countSameRenotes(
userId: string,
renoteId: string,
excludeNoteId: string | undefined,
): Promise<number> {
// 指定したユーザーの指定したノートのリノートがいくつあるか数える // 指定したユーザーの指定したノートのリノートがいくつあるか数える
const query = Notes.createQueryBuilder('note') const query = Notes.createQueryBuilder("note")
.where('note.userId = :userId', { userId }) .where("note.userId = :userId", { userId })
.andWhere('note.renoteId = :renoteId', { renoteId }); .andWhere("note.renoteId = :renoteId", { renoteId });
// 指定した投稿を除く // 指定した投稿を除く
if (excludeNoteId) { if (excludeNoteId) {
query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); query.andWhere("note.id != :excludeNoteId", { excludeNoteId });
} }
return await query.getCount(); return await query.getCount();

View File

@ -1,4 +1,4 @@
import * as tmp from 'tmp'; import * as tmp from "tmp";
export function createTemp(): Promise<[string, () => void]> { export function createTemp(): Promise<[string, () => void]> {
return new Promise<[string, () => void]>((res, rej) => { return new Promise<[string, () => void]>((res, rej) => {
@ -18,7 +18,7 @@ export function createTempDir(): Promise<[string, () => void]> {
(e, path, cleanup) => { (e, path, cleanup) => {
if (e) return rej(e); if (e) return rej(e);
res([path, cleanup]); res([path, cleanup]);
} },
); );
}); });
} }

View File

@ -1,6 +1,6 @@
import { createTemp } from './create-temp.js'; import { createTemp } from "./create-temp.js";
import { downloadUrl } from './download-url.js'; import { downloadUrl } from "./download-url.js";
import { detectType } from './get-file-info.js'; import { detectType } from "./get-file-info.js";
export async function detectUrlMime(url: string) { export async function detectUrlMime(url: string) {
const [path, cleanup] = await createTemp(); const [path, cleanup] = await createTemp();

View File

@ -1,10 +1,10 @@
import * as fs from 'node:fs'; import * as fs from "node:fs";
import * as util from 'node:util'; import * as util from "node:util";
import Logger from '@/services/logger.js'; import Logger from "@/services/logger.js";
import { createTemp } from './create-temp.js'; import { createTemp } from "./create-temp.js";
import { downloadUrl } from './download-url.js'; import { downloadUrl } from "./download-url.js";
const logger = new Logger('download-text-file'); const logger = new Logger("download-text-file");
export async function downloadTextFile(url: string): Promise<string> { export async function downloadTextFile(url: string): Promise<string> {
// Create temp file // Create temp file
@ -16,7 +16,7 @@ export async function downloadTextFile(url: string): Promise<string> {
// write content at URL to temp file // write content at URL to temp file
await downloadUrl(url, path); await downloadUrl(url, path);
const text = await util.promisify(fs.readFile)(path, 'utf8'); const text = await util.promisify(fs.readFile)(path, "utf8");
return text; return text;
} finally { } finally {

View File

@ -1,18 +1,18 @@
import * as fs from 'node:fs'; import * as fs from "node:fs";
import * as stream from 'node:stream'; import * as stream from "node:stream";
import * as util from 'node:util'; import * as util from "node:util";
import got, * as Got from 'got'; import got, * as Got from "got";
import { httpAgent, httpsAgent, StatusError } from './fetch.js'; import { httpAgent, httpsAgent, StatusError } from "./fetch.js";
import config from '@/config/index.js'; import config from "@/config/index.js";
import chalk from 'chalk'; import chalk from "chalk";
import Logger from '@/services/logger.js'; import Logger from "@/services/logger.js";
import IPCIDR from 'ip-cidr'; import IPCIDR from "ip-cidr";
import PrivateIp from 'private-ip'; import PrivateIp from "private-ip";
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline);
export async function downloadUrl(url: string, path: string): Promise<void> { export async function downloadUrl(url: string, path: string): Promise<void> {
const logger = new Logger('download'); const logger = new Logger("download");
logger.info(`Downloading ${chalk.cyan(url)} ...`); logger.info(`Downloading ${chalk.cyan(url)} ...`);
@ -20,55 +20,69 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
const operationTimeout = 60 * 1000; const operationTimeout = 60 * 1000;
const maxSize = config.maxFileSize || 262144000; const maxSize = config.maxFileSize || 262144000;
const req = got.stream(url, { const req = got
headers: { .stream(url, {
'User-Agent': config.userAgent, headers: {
}, "User-Agent": config.userAgent,
timeout: { },
lookup: timeout, timeout: {
connect: timeout, lookup: timeout,
secureConnect: timeout, connect: timeout,
socket: timeout, // read timeout secureConnect: timeout,
response: timeout, socket: timeout, // read timeout
send: timeout, response: timeout,
request: operationTimeout, // whole operation timeout send: timeout,
}, request: operationTimeout, // whole operation timeout
agent: { },
http: httpAgent, agent: {
https: httpsAgent, http: httpAgent,
}, https: httpsAgent,
http2: false, // default },
retry: { http2: false, // default
limit: 0, retry: {
}, limit: 0,
}).on('response', (res: Got.Response) => { },
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) { })
if (isPrivateIp(res.ip)) { .on("response", (res: Got.Response) => {
logger.warn(`Blocked address: ${res.ip}`); if (
req.destroy(); (process.env.NODE_ENV === "production" ||
process.env.NODE_ENV === "test") &&
!config.proxy &&
res.ip
) {
if (isPrivateIp(res.ip)) {
logger.warn(`Blocked address: ${res.ip}`);
req.destroy();
}
} }
}
const contentLength = res.headers['content-length']; const contentLength = res.headers["content-length"];
if (contentLength != null) { if (contentLength != null) {
const size = Number(contentLength); const size = Number(contentLength);
if (size > maxSize) { if (size > maxSize) {
logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`); logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`);
req.destroy();
}
}
})
.on("downloadProgress", (progress: Got.Progress) => {
if (progress.transferred > maxSize) {
logger.warn(
`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`,
);
req.destroy(); req.destroy();
} }
} });
}).on('downloadProgress', (progress: Got.Progress) => {
if (progress.transferred > maxSize) {
logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
req.destroy();
}
});
try { try {
await pipeline(req, fs.createWriteStream(path)); await 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(
`${e.response.statusCode} ${e.response.statusMessage}`,
e.response.statusCode,
e.response.statusMessage,
);
} else { } else {
throw e; throw e;
} }

View File

@ -1,4 +1,4 @@
import twemoji from 'twemoji-parser/dist/lib/regex.js'; import twemoji from "twemoji-parser/dist/lib/regex.js";
const twemojiRegex = twemoji.default; const twemojiRegex = twemoji.default;
export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); export const emojiRegex = new RegExp(`(${twemojiRegex.source})`);

View File

@ -1,10 +1,10 @@
import * as mfm from 'mfm-js'; import * as mfm from "mfm-js";
import { unique } from '@/prelude/array.js'; import { unique } from "@/prelude/array.js";
export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] {
const emojiNodes = mfm.extract(nodes, (node) => { const emojiNodes = mfm.extract(nodes, (node) => {
return (node.type === 'emojiCode' && node.props.name.length <= 100); return node.type === "emojiCode" && node.props.name.length <= 100;
}); });
return unique(emojiNodes.map(x => x.props.name)); return unique(emojiNodes.map((x) => x.props.name));
} }

View File

@ -1,9 +1,9 @@
import * as mfm from 'mfm-js'; import * as mfm from "mfm-js";
import { unique } from '@/prelude/array.js'; import { unique } from "@/prelude/array.js";
export function extractHashtags(nodes: mfm.MfmNode[]): string[] { export function extractHashtags(nodes: mfm.MfmNode[]): string[] {
const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag'); const hashtagNodes = mfm.extract(nodes, (node) => node.type === "hashtag");
const hashtags = unique(hashtagNodes.map(x => x.props.hashtag)); const hashtags = unique(hashtagNodes.map((x) => x.props.hashtag));
return hashtags; return hashtags;
} }

View File

@ -1,11 +1,13 @@
// test is located in test/extract-mentions // test is located in test/extract-mentions
import * as mfm from 'mfm-js'; import * as mfm from "mfm-js";
export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { export function extractMentions(
nodes: mfm.MfmNode[],
): mfm.MfmMention["props"][] {
// TODO: 重複を削除 // TODO: 重複を削除
const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention'); const mentionNodes = mfm.extract(nodes, (node) => node.type === "mention");
const mentions = mentionNodes.map(x => x.props); const mentions = mentionNodes.map((x) => x.props);
return mentions; return mentions;
} }

View File

@ -1,16 +1,16 @@
import { db } from '@/db/postgre.js'; import { db } from "@/db/postgre.js";
import { Meta } from '@/models/entities/meta.js'; import { Meta } from "@/models/entities/meta.js";
let cache: Meta; let cache: Meta;
export async function fetchMeta(noCache = false): Promise<Meta> { export async function fetchMeta(noCache = false): Promise<Meta> {
if (!noCache && cache) return cache; if (!noCache && cache) return cache;
return await db.transaction(async transactionalEntityManager => { return await db.transaction(async (transactionalEntityManager) => {
// New IDs are prioritized because multiple records may have been created due to past bugs. // New IDs are prioritized because multiple records may have been created due to past bugs.
const metas = await transactionalEntityManager.find(Meta, { const metas = await transactionalEntityManager.find(Meta, {
order: { order: {
id: 'DESC', id: "DESC",
}, },
}); });
@ -25,11 +25,13 @@ export async function fetchMeta(noCache = false): Promise<Meta> {
.upsert( .upsert(
Meta, Meta,
{ {
id: 'x', id: "x",
}, },
['id'], ["id"],
) )
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); .then((x) =>
transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]),
);
cache = saved; cache = saved;
return saved; return saved;
@ -38,7 +40,7 @@ export async function fetchMeta(noCache = false): Promise<Meta> {
} }
setInterval(() => { setInterval(() => {
fetchMeta(true).then(meta => { fetchMeta(true).then((meta) => {
cache = meta; cache = meta;
}); });
}, 1000 * 10); }, 1000 * 10);

View File

@ -1,9 +1,11 @@
import { fetchMeta } from './fetch-meta.js'; import { fetchMeta } from "./fetch-meta.js";
import { ILocalUser } from '@/models/entities/user.js'; import type { ILocalUser } from "@/models/entities/user.js";
import { Users } from '@/models/index.js'; import { Users } from "@/models/index.js";
export async function fetchProxyAccount(): Promise<ILocalUser | null> { export async function fetchProxyAccount(): Promise<ILocalUser | null> {
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.proxyAccountId == null) return null; if (meta.proxyAccountId == null) return null;
return await Users.findOneByOrFail({ id: meta.proxyAccountId }) as ILocalUser; return (await Users.findOneByOrFail({
id: meta.proxyAccountId,
})) as ILocalUser;
} }

View File

@ -1,40 +1,63 @@
import * as http from 'node:http'; import * as http from "node:http";
import * as https from 'node:https'; import * as https from "node:https";
import { URL } from 'node:url'; import type { URL } from "node:url";
import CacheableLookup from 'cacheable-lookup'; import CacheableLookup from "cacheable-lookup";
import fetch from 'node-fetch'; import fetch from "node-fetch";
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from "hpagent";
import config from '@/config/index.js'; import config from "@/config/index.js";
export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>) { export async function getJson(
url: string,
accept = "application/json, */*",
timeout = 10000,
headers?: Record<string, string>,
) {
const res = await getResponse({ const res = await getResponse({
url, url,
method: 'GET', method: "GET",
headers: Object.assign({ headers: Object.assign(
'User-Agent': config.userAgent, {
Accept: accept, "User-Agent": config.userAgent,
}, headers || {}), Accept: accept,
},
headers || {},
),
timeout, timeout,
}); });
return await res.json(); return await res.json();
} }
export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record<string, string>) { export async function getHtml(
url: string,
accept = "text/html, */*",
timeout = 10000,
headers?: Record<string, string>,
) {
const res = await getResponse({ const res = await getResponse({
url, url,
method: 'GET', method: "GET",
headers: Object.assign({ headers: Object.assign(
'User-Agent': config.userAgent, {
Accept: accept, "User-Agent": config.userAgent,
}, headers || {}), Accept: accept,
},
headers || {},
),
timeout, timeout,
}); });
return await res.text(); return await res.text();
} }
export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) { export async function getResponse(args: {
url: string;
method: string;
body?: string;
headers: Record<string, string>;
timeout?: number;
size?: number;
}) {
const timeout = args.timeout || 10 * 1000; const timeout = args.timeout || 10 * 1000;
const controller = new AbortController(); const controller = new AbortController();
@ -53,16 +76,20 @@ export async function getResponse(args: { url: string, method: string, body?: st
}); });
if (!res.ok) { if (!res.ok) {
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); throw new StatusError(
`${res.status} ${res.statusText}`,
res.status,
res.statusText,
);
} }
return res; return res;
} }
const cache = new CacheableLookup({ const cache = new CacheableLookup({
maxTtl: 3600, // 1hours maxTtl: 3600, // 1hours
errorTtl: 30, // 30secs errorTtl: 30, // 30secs
lookup: false, // nativeのdns.lookupにfallbackしない lookup: false, // nativeのdns.lookupにfallbackしない
}); });
/** /**
@ -90,13 +117,13 @@ const maxSockets = Math.max(256, config.deliverJobConcurrency || 128);
*/ */
export const httpAgent = config.proxy export const httpAgent = config.proxy
? new HttpProxyAgent({ ? new HttpProxyAgent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
maxSockets, maxSockets,
maxFreeSockets: 256, maxFreeSockets: 256,
scheduling: 'lifo', scheduling: "lifo",
proxy: config.proxy, proxy: config.proxy,
}) })
: _http; : _http;
/** /**
@ -104,13 +131,13 @@ export const httpAgent = config.proxy
*/ */
export const httpsAgent = config.proxy export const httpsAgent = config.proxy
? new HttpsProxyAgent({ ? new HttpsProxyAgent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
maxSockets, maxSockets,
maxFreeSockets: 256, maxFreeSockets: 256,
scheduling: 'lifo', scheduling: "lifo",
proxy: config.proxy, proxy: config.proxy,
}) })
: _https; : _https;
/** /**
@ -120,9 +147,9 @@ export const httpsAgent = config.proxy
*/ */
export function getAgentByUrl(url: URL, bypassProxy = false) { export function getAgentByUrl(url: URL, bypassProxy = false) {
if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) { if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) {
return url.protocol === 'http:' ? _http : _https; return url.protocol === "http:" ? _http : _https;
} else { } else {
return url.protocol === 'http:' ? httpAgent : httpsAgent; return url.protocol === "http:" ? httpAgent : httpsAgent;
} }
} }
@ -133,9 +160,12 @@ export class StatusError extends Error {
constructor(message: string, statusCode: number, statusMessage?: string) { constructor(message: string, statusCode: number, statusMessage?: string) {
super(message); super(message);
this.name = 'StatusError'; this.name = "StatusError";
this.statusCode = statusCode; this.statusCode = statusCode;
this.statusMessage = statusMessage; this.statusMessage = statusMessage;
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500; this.isClientError =
typeof this.statusCode === "number" &&
this.statusCode >= 400 &&
this.statusCode < 500;
} }
} }

View File

@ -1,21 +1,27 @@
import { ulid } from 'ulid'; import { ulid } from "ulid";
import { genAid } from './id/aid.js'; import { genAid } from "./id/aid.js";
import { genMeid } from './id/meid.js'; import { genMeid } from "./id/meid.js";
import { genMeidg } from './id/meidg.js'; import { genMeidg } from "./id/meidg.js";
import { genObjectId } from './id/object-id.js'; import { genObjectId } from "./id/object-id.js";
import config from '@/config/index.js'; import config from "@/config/index.js";
const metohd = config.id.toLowerCase(); const metohd = config.id.toLowerCase();
export function genId(date?: Date): string { export function genId(date?: Date): string {
if (!date || (date > new Date())) date = new Date(); if (!date || date > new Date()) date = new Date();
switch (metohd) { switch (metohd) {
case 'aid': return genAid(date); case "aid":
case 'meid': return genMeid(date); return genAid(date);
case 'meidg': return genMeidg(date); case "meid":
case 'ulid': return ulid(date.getTime()); return genMeid(date);
case 'objectid': return genObjectId(date); case "meidg":
default: throw new Error('unrecognized id generation method'); return genMeidg(date);
case "ulid":
return ulid(date.getTime());
case "objectid":
return genObjectId(date);
default:
throw new Error("unrecognized id generation method");
} }
} }

View File

@ -3,37 +3,37 @@
* https://en.wikipedia.org/wiki/Identicon * https://en.wikipedia.org/wiki/Identicon
*/ */
import { WriteStream } from 'node:fs'; import type { WriteStream } from "node:fs";
import * as p from 'pureimage'; import * as p from "pureimage";
import gen from 'random-seed'; import gen from "random-seed";
const size = 128; // px const size = 128; // px
const n = 5; // resolution const n = 5; // resolution
const margin = (size / 4); const margin = size / 4;
const colors = [ const colors = [
['#FF512F', '#DD2476'], ["#FF512F", "#DD2476"],
['#FF61D2', '#FE9090'], ["#FF61D2", "#FE9090"],
['#72FFB6', '#10D164'], ["#72FFB6", "#10D164"],
['#FD8451', '#FFBD6F'], ["#FD8451", "#FFBD6F"],
['#305170', '#6DFC6B'], ["#305170", "#6DFC6B"],
['#00C0FF', '#4218B8'], ["#00C0FF", "#4218B8"],
['#009245', '#FCEE21'], ["#009245", "#FCEE21"],
['#0100EC', '#FB36F4'], ["#0100EC", "#FB36F4"],
['#FDABDD', '#374A5A'], ["#FDABDD", "#374A5A"],
['#38A2D7', '#561139'], ["#38A2D7", "#561139"],
['#121C84', '#8278DA'], ["#121C84", "#8278DA"],
['#5761B2', '#1FC5A8'], ["#5761B2", "#1FC5A8"],
['#FFDB01', '#0E197D'], ["#FFDB01", "#0E197D"],
['#FF3E9D', '#0E1F40'], ["#FF3E9D", "#0E1F40"],
['#766eff', '#00d4ff'], ["#766eff", "#00d4ff"],
['#9bff6e', '#00d4ff'], ["#9bff6e", "#00d4ff"],
['#ff6e94', '#00d4ff'], ["#ff6e94", "#00d4ff"],
['#ffa96e', '#00d4ff'], ["#ffa96e", "#00d4ff"],
['#ffa96e', '#ff009d'], ["#ffa96e", "#ff009d"],
['#ffdd6e', '#ff009d'], ["#ffdd6e", "#ff009d"],
]; ];
const actualSize = size - (margin * 2); const actualSize = size - margin * 2;
const cellSize = actualSize / n; const cellSize = actualSize / n;
const sideN = Math.floor(n / 2); const sideN = Math.floor(n / 2);
@ -43,7 +43,7 @@ const sideN = Math.floor(n / 2);
export function genIdenticon(seed: string, stream: WriteStream): Promise<void> { export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
const rand = gen.create(seed); const rand = gen.create(seed);
const canvas = p.make(size, size, undefined); const canvas = p.make(size, size, undefined);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
const bgColors = colors[rand(colors.length)]; const bgColors = colors[rand(colors.length)];
@ -55,7 +55,7 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
ctx.beginPath(); ctx.beginPath();
ctx.fillRect(0, 0, size, size); ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = "#ffffff";
// side bitmap (filled by false) // side bitmap (filled by false)
const side: boolean[][] = new Array(sideN); const side: boolean[][] = new Array(sideN);
@ -66,7 +66,6 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
// 1*n (filled by false) // 1*n (filled by false)
const center: boolean[] = new Array(n).fill(false); const center: boolean[] = new Array(n).fill(false);
// eslint:disable-next-line:prefer-for-of
for (let x = 0; x < side.length; x++) { for (let x = 0; x < side.length; x++) {
for (let y = 0; y < side[x].length; y++) { for (let y = 0; y < side[x].length; y++) {
side[x][y] = rand(3) === 0; side[x][y] = rand(3) === 0;
@ -80,17 +79,17 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
// Draw // Draw
for (let x = 0; x < n; x++) { for (let x = 0; x < n; x++) {
for (let y = 0; y < n; y++) { for (let y = 0; y < n; y++) {
const isXCenter = x === ((n - 1) / 2); const isXCenter = x === (n - 1) / 2;
if (isXCenter && !center[y]) continue; if (isXCenter && !center[y]) continue;
const isLeftSide = x < ((n - 1) / 2); const isLeftSide = x < (n - 1) / 2;
if (isLeftSide && !side[x][y]) continue; if (isLeftSide && !side[x][y]) continue;
const isRightSide = x > ((n - 1) / 2); const isRightSide = x > (n - 1) / 2;
if (isRightSide && !side[sideN - (x - sideN)][y]) continue; if (isRightSide && !side[sideN - (x - sideN)][y]) continue;
const actualX = margin + (cellSize * x); const actualX = margin + cellSize * x;
const actualY = margin + (cellSize * y); const actualY = margin + cellSize * y;
ctx.beginPath(); ctx.beginPath();
ctx.fillRect(actualX, actualY, cellSize, cellSize); ctx.fillRect(actualX, actualY, cellSize, cellSize);
} }

View File

@ -1,34 +1,40 @@
import * as crypto from 'node:crypto'; import * as crypto from "node:crypto";
import * as util from 'node:util'; import * as util from "node:util";
const generateKeyPair = util.promisify(crypto.generateKeyPair); const generateKeyPair = util.promisify(crypto.generateKeyPair);
export async function genRsaKeyPair(modulusLength = 2048) { export async function genRsaKeyPair(modulusLength = 2048) {
return await generateKeyPair('rsa', { return await generateKeyPair("rsa", {
modulusLength, modulusLength,
publicKeyEncoding: { publicKeyEncoding: {
type: 'spki', type: "spki",
format: 'pem', format: "pem",
}, },
privateKeyEncoding: { privateKeyEncoding: {
type: 'pkcs8', type: "pkcs8",
format: 'pem', format: "pem",
cipher: undefined, cipher: undefined,
passphrase: undefined, passphrase: undefined,
}, },
}); });
} }
export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'secp521r1' | 'curve25519' = 'prime256v1') { export async function genEcKeyPair(
return await generateKeyPair('ec', { namedCurve:
| "prime256v1"
| "secp384r1"
| "secp521r1"
| "curve25519" = "prime256v1",
) {
return await generateKeyPair("ec", {
namedCurve, namedCurve,
publicKeyEncoding: { publicKeyEncoding: {
type: 'spki', type: "spki",
format: 'pem', format: "pem",
}, },
privateKeyEncoding: { privateKeyEncoding: {
type: 'pkcs8', type: "pkcs8",
format: 'pem', format: "pem",
cipher: undefined, cipher: undefined,
passphrase: undefined, passphrase: undefined,
}, },

View File

@ -1,18 +1,18 @@
import * as fs from 'node:fs'; import * as fs from "node:fs";
import * as crypto from 'node:crypto'; import * as crypto from "node:crypto";
import { join } from 'node:path'; import { join } from "node:path";
import * as stream from 'node:stream'; import * as stream from "node:stream";
import * as util from 'node:util'; import * as util from "node:util";
import { FSWatcher } from 'chokidar'; import { FSWatcher } from "chokidar";
import { fileTypeFromFile } from 'file-type'; import { fileTypeFromFile } from "file-type";
import FFmpeg from 'fluent-ffmpeg'; import FFmpeg from "fluent-ffmpeg";
import isSvg from 'is-svg'; import isSvg from "is-svg";
import probeImageSize from 'probe-image-size'; import probeImageSize from "probe-image-size";
import { type predictionType } from 'nsfwjs'; import { type predictionType } from "nsfwjs";
import sharp from 'sharp'; import sharp from "sharp";
import { encode } from 'blurhash'; import { encode } from "blurhash";
import { detectSensitive } from '@/services/detect-sensitive.js'; import { detectSensitive } from "@/services/detect-sensitive.js";
import { createTempDir } from './create-temp.js'; import { createTempDir } from "./create-temp.js";
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline);
@ -33,24 +33,27 @@ export type FileInfo = {
}; };
const TYPE_OCTET_STREAM = { const TYPE_OCTET_STREAM = {
mime: 'application/octet-stream', mime: "application/octet-stream",
ext: null, ext: null,
}; };
const TYPE_SVG = { const TYPE_SVG = {
mime: 'image/svg+xml', mime: "image/svg+xml",
ext: 'svg', ext: "svg",
}; };
/** /**
* Get file information * Get file information
*/ */
export async function getFileInfo(path: string, opts: { export async function getFileInfo(
skipSensitiveDetection: boolean; path: string,
sensitiveThreshold?: number; opts: {
sensitiveThresholdForPorn?: number; skipSensitiveDetection: boolean;
enableSensitiveMediaDetectionForVideos?: boolean; sensitiveThreshold?: number;
}): Promise<FileInfo> { sensitiveThresholdForPorn?: number;
enableSensitiveMediaDetectionForVideos?: boolean;
},
): Promise<FileInfo> {
const warnings = [] as string[]; const warnings = [] as string[];
const size = await getFileSize(path); const size = await getFileSize(path);
@ -63,24 +66,37 @@ export async function getFileInfo(path: string, opts: {
let height: number | undefined; let height: number | undefined;
let orientation: number | undefined; let orientation: number | undefined;
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop', 'image/avif'].includes(type.mime)) { if (
const imageSize = await detectImageSize(path).catch(e => { [
"image/jpeg",
"image/gif",
"image/png",
"image/apng",
"image/webp",
"image/bmp",
"image/tiff",
"image/svg+xml",
"image/vnd.adobe.photoshop",
"image/avif",
].includes(type.mime)
) {
const imageSize = await detectImageSize(path).catch((e) => {
warnings.push(`detectImageSize failed: ${e}`); warnings.push(`detectImageSize failed: ${e}`);
return undefined; return undefined;
}); });
// うまく判定できない画像は octet-stream にする // うまく判定できない画像は octet-stream にする
if (!imageSize) { if (!imageSize) {
warnings.push('cannot detect image dimensions'); warnings.push("cannot detect image dimensions");
type = TYPE_OCTET_STREAM; type = TYPE_OCTET_STREAM;
} else if (imageSize.wUnits === 'px') { } else if (imageSize.wUnits === "px") {
width = imageSize.width; width = imageSize.width;
height = imageSize.height; height = imageSize.height;
orientation = imageSize.orientation; orientation = imageSize.orientation;
// 制限を超えている画像は octet-stream にする // 制限を超えている画像は octet-stream にする
if (imageSize.width > 16383 || imageSize.height > 16383) { if (imageSize.width > 16383 || imageSize.height > 16383) {
warnings.push('image dimensions exceeds limits'); warnings.push("image dimensions exceeds limits");
type = TYPE_OCTET_STREAM; type = TYPE_OCTET_STREAM;
} }
} else { } else {
@ -90,8 +106,18 @@ export async function getFileInfo(path: string, opts: {
let blurhash: string | undefined; let blurhash: string | undefined;
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml', 'image/avif'].includes(type.mime)) { if (
blurhash = await getBlurhash(path).catch(e => { [
"image/jpeg",
"image/gif",
"image/png",
"image/apng",
"image/webp",
"image/svg+xml",
"image/avif",
].includes(type.mime)
) {
blurhash = await getBlurhash(path).catch((e) => {
warnings.push(`getBlurhash failed: ${e}`); warnings.push(`getBlurhash failed: ${e}`);
return undefined; return undefined;
}); });
@ -107,11 +133,14 @@ export async function getFileInfo(path: string, opts: {
opts.sensitiveThreshold ?? 0.5, opts.sensitiveThreshold ?? 0.5,
opts.sensitiveThresholdForPorn ?? 0.75, opts.sensitiveThresholdForPorn ?? 0.75,
opts.enableSensitiveMediaDetectionForVideos ?? false, opts.enableSensitiveMediaDetectionForVideos ?? false,
).then(value => { ).then(
[sensitive, porn] = value; (value) => {
}, error => { [sensitive, porn] = value;
warnings.push(`detectSensitivity failed: ${error}`); },
}); (error) => {
warnings.push(`detectSensitivity failed: ${error}`);
},
);
} }
return { return {
@ -128,71 +157,100 @@ export async function getFileInfo(path: string, opts: {
}; };
} }
async function detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { async function detectSensitivity(
source: string,
mime: string,
sensitiveThreshold: number,
sensitiveThresholdForPorn: number,
analyzeVideo: boolean,
): Promise<[sensitive: boolean, porn: boolean]> {
let sensitive = false; let sensitive = false;
let porn = false; let porn = false;
function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] { function judgePrediction(
result: readonly predictionType[],
): [sensitive: boolean, porn: boolean] {
let sensitive = false; let sensitive = false;
let porn = false; let porn = false;
if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if (
if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true; (result.find((x) => x.className === "Sexy")?.probability ?? 0) >
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true; sensitiveThreshold
)
sensitive = true;
if (
(result.find((x) => x.className === "Hentai")?.probability ?? 0) >
sensitiveThreshold
)
sensitive = true;
if (
(result.find((x) => x.className === "Porn")?.probability ?? 0) >
sensitiveThreshold
)
sensitive = true;
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true; if (
(result.find((x) => x.className === "Porn")?.probability ?? 0) >
sensitiveThresholdForPorn
)
porn = true;
return [sensitive, porn]; return [sensitive, porn];
} }
if (['image/jpeg', 'image/png', 'image/webp'].includes(mime)) { if (["image/jpeg", "image/png", "image/webp"].includes(mime)) {
const result = await detectSensitive(source); const result = await detectSensitive(source);
if (result) { if (result) {
[sensitive, porn] = judgePrediction(result); [sensitive, porn] = judgePrediction(result);
} }
} else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) { } else if (
analyzeVideo &&
(mime === "image/apng" || mime.startsWith("video/"))
) {
const [outDir, disposeOutDir] = await createTempDir(); const [outDir, disposeOutDir] = await createTempDir();
try { try {
const command = FFmpeg() const command = FFmpeg()
.input(source) .input(source)
.inputOptions([ .inputOptions([
'-skip_frame', 'nokey', // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない) "-skip_frame",
'-lowres', '3', // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない) "nokey", // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない)
"-lowres",
"3", // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない)
]) ])
.noAudio() .noAudio()
.videoFilters([ .videoFilters([
{ {
filter: 'select', // フレームのフィルタリング filter: "select", // フレームのフィルタリング
options: { options: {
e: 'eq(pict_type,PICT_TYPE_I)', // I-Frame のみをフィルタするVP9 とかはデコードしてみないとわからないっぽい) e: "eq(pict_type,PICT_TYPE_I)", // I-Frame のみをフィルタするVP9 とかはデコードしてみないとわからないっぽい)
}, },
}, },
{ {
filter: 'blackframe', // 暗いフレームの検出 filter: "blackframe", // 暗いフレームの検出
options: { options: {
amount: '0', // 暗さに関わらず全てのフレームで測定値を取る amount: "0", // 暗さに関わらず全てのフレームで測定値を取る
}, },
}, },
{ {
filter: 'metadata', filter: "metadata",
options: { options: {
mode: 'select', // フレーム選択モード mode: "select", // フレーム選択モード
key: 'lavfi.blackframe.pblack', // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する) key: "lavfi.blackframe.pblack", // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する)
value: '50', value: "50",
function: 'less', // 50% 未満のフレームを選択する50% 以上暗部があるフレームだと誤検知を招くかもしれないので) function: "less", // 50% 未満のフレームを選択する50% 以上暗部があるフレームだと誤検知を招くかもしれないので)
}, },
}, },
{ {
filter: 'scale', filter: "scale",
options: { options: {
w: 299, w: 299,
h: 299, h: 299,
}, },
}, },
]) ])
.format('image2') .format("image2")
.output(join(outDir, '%d.png')) .output(join(outDir, "%d.png"))
.outputOptions(['-vsync', '0']); // 可変フレームレートにすることで穴埋めをさせない .outputOptions(["-vsync", "0"]); // 可変フレームレートにすることで穴埋めをさせない
const results: ReturnType<typeof judgePrediction>[] = []; const results: ReturnType<typeof judgePrediction>[] = [];
let frameIndex = 0; let frameIndex = 0;
let targetIndex = 0; let targetIndex = 0;
@ -213,8 +271,12 @@ async function detectSensitivity(source: string, mime: string, sensitiveThreshol
fs.promises.unlink(path); fs.promises.unlink(path);
} }
} }
sensitive = results.filter(x => x[0]).length >= Math.ceil(results.length * sensitiveThreshold); sensitive =
porn = results.filter(x => x[1]).length >= Math.ceil(results.length * sensitiveThresholdForPorn); results.filter((x) => x[0]).length >=
Math.ceil(results.length * sensitiveThreshold);
porn =
results.filter((x) => x[1]).length >=
Math.ceil(results.length * sensitiveThresholdForPorn);
} finally { } finally {
disposeOutDir(); disposeOutDir();
} }
@ -223,35 +285,39 @@ async function detectSensitivity(source: string, mime: string, sensitiveThreshol
return [sensitive, porn]; return [sensitive, porn];
} }
async function* asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> { async function* asyncIterateFrames(
cwd: string,
command: FFmpeg.FfmpegCommand,
): AsyncGenerator<string, void> {
const watcher = new FSWatcher({ const watcher = new FSWatcher({
cwd, cwd,
disableGlobbing: true, disableGlobbing: true,
}); });
let finished = false; let finished = false;
command.once('end', () => { command.once("end", () => {
finished = true; finished = true;
watcher.close(); watcher.close();
}); });
command.run(); command.run();
for (let i = 1; true; i++) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition for (let i = 1; true; i++) {
const current = `${i}.png`; const current = `${i}.png`;
const next = `${i + 1}.png`; const next = `${i + 1}.png`;
const framePath = join(cwd, current); const framePath = join(cwd, current);
if (await exists(join(cwd, next))) { if (await exists(join(cwd, next))) {
yield framePath; yield framePath;
} else if (!finished) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition } else if (!finished) {
watcher.add(next); watcher.add(next);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
watcher.on('add', function onAdd(path) { watcher.on("add", function onAdd(path) {
if (path === next) { // 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている if (path === next) {
// 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている
watcher.unwatch(current); watcher.unwatch(current);
watcher.off('add', onAdd); watcher.off("add", onAdd);
resolve(); resolve();
} }
}); });
command.once('end', resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている command.once("end", resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている
command.once('error', reject); command.once("error", reject);
}); });
yield framePath; yield framePath;
} else if (await exists(framePath)) { } else if (await exists(framePath)) {
@ -263,7 +329,10 @@ async function* asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand):
} }
function exists(path: string): Promise<boolean> { function exists(path: string): Promise<boolean> {
return fs.promises.access(path).then(() => true, () => false); return fs.promises.access(path).then(
() => true,
() => false,
);
} }
/** /**
@ -283,7 +352,7 @@ export async function detectType(path: string): Promise<{
if (type) { if (type) {
// XMLはSVGかもしれない // XMLはSVGかもしれない
if (type.mime === 'application/xml' && await checkSvg(path)) { if (type.mime === "application/xml" && (await checkSvg(path))) {
return TYPE_SVG; return TYPE_SVG;
} }
@ -327,7 +396,7 @@ export async function getFileSize(path: string): Promise<number> {
* Calculate MD5 hash * Calculate MD5 hash
*/ */
async function calcHash(path: string): Promise<string> { async function calcHash(path: string): Promise<string> {
const hash = crypto.createHash('md5').setEncoding('hex'); const hash = crypto.createHash("md5").setEncoding("hex");
await pipeline(fs.createReadStream(path), hash); await pipeline(fs.createReadStream(path), hash);
return hash.read(); return hash.read();
} }
@ -356,7 +425,7 @@ function getBlurhash(path: string): Promise<string> {
sharp(path) sharp(path)
.raw() .raw()
.ensureAlpha() .ensureAlpha()
.resize(64, 64, { fit: 'inside' }) .resize(64, 64, { fit: "inside" })
.toBuffer((err, buffer, { width, height }) => { .toBuffer((err, buffer, { width, height }) => {
if (err) return reject(err); if (err) return reject(err);

View File

@ -1,9 +1,9 @@
import IPCIDR from 'ip-cidr'; import IPCIDR from "ip-cidr";
export function getIpHash(ip: string) { export function getIpHash(ip: string) {
// because a single person may control many IPv6 addresses, // because a single person may control many IPv6 addresses,
// only a /64 subnet prefix of any IP will be taken into account. // only a /64 subnet prefix of any IP will be taken into account.
// (this means for IPv4 the entire address is used) // (this means for IPv4 the entire address is used)
const prefix = IPCIDR.createAddress(ip).mask(64); const prefix = IPCIDR.createAddress(ip).mask(64);
return 'ip-' + BigInt('0b' + prefix).toString(36); return `ip-${BigInt(`0b${prefix}`).toString(36)}`;
} }

View File

@ -1,21 +1,21 @@
import { Packed } from './schema.js'; import type { Packed } from "./schema.js";
/** /**
* 稿 * 稿
* @param {*} note (packされた)稿 * @param {*} note (packされた)稿
*/ */
export const getNoteSummary = (note: Packed<'Note'>): string => { export const getNoteSummary = (note: Packed<"Note">): string => {
if (note.deletedAt) { if (note.deletedAt) {
return ``; return "❌";
} }
let summary = ''; let summary = "";
// 本文 // 本文
if (note.cw != null) { if (note.cw != null) {
summary += note.cw; summary += note.cw;
} else { } else {
summary += note.text ? note.text : ''; summary += note.text ? note.text : "";
} }
// ファイルが添付されているとき // ファイルが添付されているとき
@ -25,7 +25,7 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
// 投票が添付されているとき // 投票が添付されているとき
if (note.poll) { if (note.poll) {
summary += ` (📊)`; summary += " (📊)";
} }
/* /*

View File

@ -1,16 +1,28 @@
export default function(reaction: string): string { export default function (reaction: string): string {
switch (reaction) { switch (reaction) {
case 'like': return '👍'; case "like":
case 'love': return '❤️'; return "👍";
case 'laugh': return '😆'; case "love":
case 'hmm': return '🤔'; return "❤️";
case 'surprise': return '😮'; case "laugh":
case 'congrats': return '🎉'; return "😆";
case 'angry': return '💢'; case "hmm":
case 'confused': return '😥'; return "🤔";
case 'rip': return '😇'; case "surprise":
case 'pudding': return '🍮'; return "😮";
case 'star': return '⭐'; case "congrats":
default: return reaction; return "🎉";
case "angry":
return "💢";
case "confused":
return "😥";
case "rip":
return "😇";
case "pudding":
return "🍮";
case "star":
return "⭐";
default:
return reaction;
} }
} }

View File

@ -1,4 +1,3 @@
// If you change DB_* values, you must also change the DB schema. // If you change DB_* values, you must also change the DB schema.
/** /**

View File

@ -13,7 +13,7 @@ export class I18n<T extends Record<string, any>> {
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
public t(key: string, args?: Record<string, any>): string { public t(key: string, args?: Record<string, any>): string {
try { try {
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string; let str = key.split(".").reduce((o, i) => o[i], this.locale) as string;
if (args) { if (args) {
for (const [k, v] of Object.entries(args)) { for (const [k, v] of Object.entries(args)) {

View File

@ -1,7 +1,7 @@
// AID // AID
// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列] // 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列]
import * as crypto from 'node:crypto'; import * as crypto from "node:crypto";
const TIME2000 = 946684800000; const TIME2000 = 946684800000;
let counter = crypto.randomBytes(2).readUInt16LE(0); let counter = crypto.randomBytes(2).readUInt16LE(0);
@ -10,16 +10,16 @@ function getTime(time: number) {
time = time - TIME2000; time = time - TIME2000;
if (time < 0) time = 0; if (time < 0) time = 0;
return time.toString(36).padStart(8, '0'); return time.toString(36).padStart(8, "0");
} }
function getNoise() { function getNoise() {
return counter.toString(36).padStart(2, '0').slice(-2); return counter.toString(36).padStart(2, "0").slice(-2);
} }
export function genAid(date: Date): string { export function genAid(date: Date): string {
const t = date.getTime(); const t = date.getTime();
if (isNaN(t)) throw 'Failed to create AID: Invalid Date'; if (isNaN(t)) throw "Failed to create AID: Invalid Date";
counter++; counter++;
return getTime(t) + getNoise(); return getTime(t) + getNoise();
} }

View File

@ -1,4 +1,4 @@
const CHARS = '0123456789abcdef'; const CHARS = "0123456789abcdef";
function getTime(time: number) { function getTime(time: number) {
if (time < 0) time = 0; if (time < 0) time = 0;
@ -12,7 +12,7 @@ function getTime(time: number) {
} }
function getRandom() { function getRandom() {
let str = ''; let str = "";
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
str += CHARS[Math.floor(Math.random() * CHARS.length)]; str += CHARS[Math.floor(Math.random() * CHARS.length)];

View File

@ -1,4 +1,4 @@
const CHARS = '0123456789abcdef'; const CHARS = "0123456789abcdef";
// 4bit Fixed hex value 'g' // 4bit Fixed hex value 'g'
// 44bit UNIX Time ms in Hex // 44bit UNIX Time ms in Hex
@ -14,7 +14,7 @@ function getTime(time: number) {
} }
function getRandom() { function getRandom() {
let str = ''; let str = "";
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
str += CHARS[Math.floor(Math.random() * CHARS.length)]; str += CHARS[Math.floor(Math.random() * CHARS.length)];
@ -24,5 +24,5 @@ function getRandom() {
} }
export function genMeidg(date: Date): string { export function genMeidg(date: Date): string {
return 'g' + getTime(date.getTime()) + getRandom(); return `g${getTime(date.getTime())}${getRandom()}`;
} }

View File

@ -1,4 +1,4 @@
const CHARS = '0123456789abcdef'; const CHARS = "0123456789abcdef";
function getTime(time: number) { function getTime(time: number) {
if (time < 0) time = 0; if (time < 0) time = 0;
@ -12,7 +12,7 @@ function getTime(time: number) {
} }
function getRandom() { function getRandom() {
let str = ''; let str = "";
for (let i = 0; i < 16; i++) { for (let i = 0; i < 16; i++) {
str += CHARS[Math.floor(Math.random() * CHARS.length)]; str += CHARS[Math.floor(Math.random() * CHARS.length)];

View File

@ -7,7 +7,7 @@ export class IdentifiableError extends Error {
constructor(id: string, message?: string) { constructor(id: string, message?: string) {
super(message); super(message);
this.message = message || ''; this.message = message || "";
this.id = id; this.id = id;
} }
} }

View File

@ -1,3 +1,5 @@
export function isDuplicateKeyValueError(e: unknown | Error): boolean { export function isDuplicateKeyValueError(e: unknown | Error): boolean {
return (e as any).message && (e as Error).message.startsWith('duplicate key value'); return (
(e as Error).message?.startsWith("duplicate key value")
);
} }

View File

@ -1,15 +1,21 @@
import { Packed } from './schema.js'; import type { Packed } from "./schema.js";
export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set<string>): boolean { export function isInstanceMuted(
if (mutedInstances.has(note?.user?.host ?? '')) return true; note: Packed<"Note">,
if (mutedInstances.has(note?.reply?.user?.host ?? '')) return true; mutedInstances: Set<string>,
if (mutedInstances.has(note?.renote?.user?.host ?? '')) return true; ): boolean {
if (mutedInstances.has(note?.user?.host ?? "")) return true;
if (mutedInstances.has(note?.reply?.user?.host ?? "")) return true;
if (mutedInstances.has(note?.renote?.user?.host ?? "")) return true;
return false; return false;
} }
export function isUserFromMutedInstance(notif: Packed<'Notification'>, mutedInstances: Set<string>): boolean { export function isUserFromMutedInstance(
if (mutedInstances.has(notif?.user?.host ?? '')) return true; notif: Packed<"Notification">,
mutedInstances: Set<string>,
): boolean {
if (mutedInstances.has(notif?.user?.host ?? "")) return true;
return false; return false;
} }

View File

@ -1,8 +1,20 @@
import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import { FILE_TYPE_BROWSERSAFE } from "@/const.js";
const dictionary = { const dictionary = {
'safe-file': FILE_TYPE_BROWSERSAFE, "safe-file": FILE_TYPE_BROWSERSAFE,
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml', 'image/avif'], "sharp-convertible-image": [
"image/jpeg",
"image/png",
"image/gif",
"image/apng",
"image/vnd.mozilla.apng",
"image/webp",
"image/svg+xml",
"image/avif",
],
}; };
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime); export const isMimeImage = (
mime: string,
type: keyof typeof dictionary,
): boolean => dictionary[type].includes(mime);

View File

@ -1,5 +1,10 @@
import { Note } from '@/models/entities/note.js'; import type { Note } from "@/models/entities/note.js";
export default function(note: Note): boolean { export default function (note: Note): boolean {
return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); return (
note.renoteId != null &&
(note.text != null ||
note.hasPoll ||
(note.fileIds != null && note.fileIds.length > 0))
);
} }

View File

@ -1,7 +1,8 @@
export function isUserRelated(note: any, ids: Set<string>): boolean { export function isUserRelated(note: any, ids: Set<string>): boolean {
if (ids.has(note.userId)) return true; // note author is muted if (ids.has(note.userId)) return true; // note author is muted
if (note.mentions && note.mentions.some((user: string) => ids.has(user))) return true; // any of mentioned users are muted if (note.mentions?.some((user: string) => ids.has(user)))
if (note.reply && isUserRelated(note.reply, ids)) return true; // also check reply target return true; // any of mentioned users are muted
if (note.reply && isUserRelated(note.reply, ids)) return true; // also check reply target
if (note.renote && isUserRelated(note.renote, ids)) return true; // also check renote target if (note.renote && isUserRelated(note.renote, ids)) return true; // also check renote target
return false; return false;
} }

View File

@ -1,10 +1,12 @@
import { UserKeypairs } from '@/models/index.js'; import { UserKeypairs } from "@/models/index.js";
import { User } from '@/models/entities/user.js'; import type { User } from "@/models/entities/user.js";
import { UserKeypair } from '@/models/entities/user-keypair.js'; import type { UserKeypair } from "@/models/entities/user-keypair.js";
import { Cache } from './cache.js'; import { Cache } from "./cache.js";
const cache = new Cache<UserKeypair>(Infinity); const cache = new Cache<UserKeypair>(Infinity);
export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> { export async function getUserKeypair(userId: User["id"]): Promise<UserKeypair> {
return await cache.fetch(userId, () => UserKeypairs.findOneByOrFail({ userId: userId })); return await cache.fetch(userId, () =>
UserKeypairs.findOneByOrFail({ userId: userId }),
);
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,5 +2,5 @@ export function normalizeForSearch(tag: string): string {
// ref. // ref.
// - https://analytics-note.xyz/programming/unicode-normalization-forms/ // - https://analytics-note.xyz/programming/unicode-normalization-forms/
// - https://maku77.github.io/js/string/normalize.html // - https://maku77.github.io/js/string/normalize.html
return tag.normalize('NFKC').toLowerCase(); return tag.normalize("NFKC").toLowerCase();
} }

View File

@ -1,15 +1,21 @@
export function nyaize(text: string): string { export function nyaize(text: string): string {
return text return (
// ja-JP text
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ') // ja-JP
// en-US .replace(/な/g, "にゃ")
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya') .replace(/ナ/g, "ニャ")
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan') .replace(/ナ/g, "ニャ")
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan') // en-US
// ko-KR .replace(/(?<=n)a/gi, (x) => (x === "A" ? "YA" : "ya"))
.replace(/[나-낳]/g, match => String.fromCharCode( .replace(/(?<=morn)ing/gi, (x) => (x === "ING" ? "YAN" : "yan"))
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0) .replace(/(?<=every)one/gi, (x) => (x === "ONE" ? "NYAN" : "nyan"))
)) // ko-KR
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥') .replace(/[나-낳]/g, (match) =>
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥'); String.fromCharCode(
match.charCodeAt(0)! + "냐".charCodeAt(0) - "나".charCodeAt(0),
),
)
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, "다냥")
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, "냥")
);
} }

View File

@ -1,12 +1,12 @@
import { In, IsNull } from 'typeorm'; import { In, IsNull } from "typeorm";
import { Emojis } from '@/models/index.js'; import { Emojis } from "@/models/index.js";
import { Emoji } from '@/models/entities/emoji.js'; import type { Emoji } from "@/models/entities/emoji.js";
import { Note } from '@/models/entities/note.js'; import type { Note } from "@/models/entities/note.js";
import { Cache } from './cache.js'; import { Cache } from "./cache.js";
import { isSelfHost, toPunyNullable } from './convert-host.js'; import { isSelfHost, toPunyNullable } from "./convert-host.js";
import { decodeReaction } from './reaction-lib.js'; import { decodeReaction } from "./reaction-lib.js";
import config from '@/config/index.js'; import config from "@/config/index.js";
import { query } from '@/prelude/url.js'; import { query } from "@/prelude/url.js";
const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12); const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
@ -18,12 +18,19 @@ type PopulatedEmoji = {
url: string; url: string;
}; };
function normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { function normalizeHost(
src: string | undefined,
noteUserHost: string | null,
): string | null {
// クエリに使うホスト // クエリに使うホスト
let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ) let host =
: src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない) src === "."
: isSelfHost(src) ? null // 自ホスト指定 ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
: (src || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない) : src === undefined
? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
: isSelfHost(src)
? null // 自ホスト指定
: src || noteUserHost; // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
host = toPunyNullable(host); host = toPunyNullable(host);
@ -48,14 +55,18 @@ function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
* @param noteUserHost * @param noteUserHost
* @returns , nullは未マッチを意味する * @returns , nullは未マッチを意味する
*/ */
export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> { export async function populateEmoji(
emojiName: string,
noteUserHost: string | null,
): Promise<PopulatedEmoji | null> {
const { name, host } = parseEmojiStr(emojiName, noteUserHost); const { name, host } = parseEmojiStr(emojiName, noteUserHost);
if (name == null) return null; if (name == null) return null;
const queryOrNull = async () => (await Emojis.findOneBy({ const queryOrNull = async () =>
name, (await Emojis.findOneBy({
host: host ?? IsNull(), name,
})) || null; host: host ?? IsNull(),
})) || null;
const emoji = await cache.fetch(`${name} ${host}`, queryOrNull); const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
@ -63,7 +74,11 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
const isLocal = emoji.host == null; const isLocal = emoji.host == null;
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため
const url = isLocal ? emojiUrl : `${config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`; const url = isLocal
? emojiUrl
: `${config.url}/proxy/${encodeURIComponent(
new URL(emojiUrl).pathname,
)}?${query({ url: emojiUrl })}`;
return { return {
name: emojiName, name: emojiName,
@ -74,51 +89,76 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
/** /**
* (, ) * (, )
*/ */
export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<PopulatedEmoji[]> { export async function populateEmojis(
const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost))); emojiNames: string[],
noteUserHost: string | null,
): Promise<PopulatedEmoji[]> {
const emojis = await Promise.all(
emojiNames.map((x) => populateEmoji(x, noteUserHost)),
);
return emojis.filter((x): x is PopulatedEmoji => x != null); return emojis.filter((x): x is PopulatedEmoji => x != null);
} }
export function aggregateNoteEmojis(notes: Note[]) { export function aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null; }[] = []; let emojis: { name: string | null; host: string | null }[] = [];
for (const note of notes) { for (const note of notes) {
emojis = emojis.concat(note.emojis emojis = emojis.concat(
.map(e => parseEmojiStr(e, note.userHost))); note.emojis.map((e) => parseEmojiStr(e, note.userHost)),
);
if (note.renote) { if (note.renote) {
emojis = emojis.concat(note.renote.emojis emojis = emojis.concat(
.map(e => parseEmojiStr(e, note.renote!.userHost))); note.renote.emojis.map((e) => parseEmojiStr(e, note.renote!.userHost)),
);
if (note.renote.user) { if (note.renote.user) {
emojis = emojis.concat(note.renote.user.emojis emojis = emojis.concat(
.map(e => parseEmojiStr(e, note.renote!.userHost))); note.renote.user.emojis.map((e) =>
parseEmojiStr(e, note.renote!.userHost),
),
);
} }
} }
const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name != null) as typeof emojis; const customReactions = Object.keys(note.reactions)
.map((x) => decodeReaction(x))
.filter((x) => x.name != null) as typeof emojis;
emojis = emojis.concat(customReactions); emojis = emojis.concat(customReactions);
if (note.user) { if (note.user) {
emojis = emojis.concat(note.user.emojis emojis = emojis.concat(
.map(e => parseEmojiStr(e, note.userHost))); note.user.emojis.map((e) => parseEmojiStr(e, note.userHost)),
);
} }
} }
return emojis.filter(x => x.name != null) as { name: string; host: string | null; }[]; return emojis.filter((x) => x.name != null) as {
name: string;
host: string | null;
}[];
} }
/** /**
* *
*/ */
export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> { export async function prefetchEmojis(
const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null); emojis: { name: string; host: string | null }[],
): Promise<void> {
const notCachedEmojis = emojis.filter(
(emoji) => cache.get(`${emoji.name} ${emoji.host}`) == null,
);
const emojisQuery: any[] = []; const emojisQuery: any[] = [];
const hosts = new Set(notCachedEmojis.map(e => e.host)); const hosts = new Set(notCachedEmojis.map((e) => e.host));
for (const host of hosts) { for (const host of hosts) {
emojisQuery.push({ emojisQuery.push({
name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)), name: In(
notCachedEmojis.filter((e) => e.host === host).map((e) => e.name),
),
host: host ?? IsNull(), host: host ?? IsNull(),
}); });
} }
const _emojis = emojisQuery.length > 0 ? await Emojis.find({ const _emojis =
where: emojisQuery, emojisQuery.length > 0
select: ['name', 'host', 'originalUrl', 'publicUrl'], ? await Emojis.find({
}) : []; where: emojisQuery,
select: ["name", "host", "originalUrl", "publicUrl"],
})
: [];
for (const emoji of _emojis) { for (const emoji of _emojis) {
cache.set(`${emoji.name} ${emoji.host}`, emoji); cache.set(`${emoji.name} ${emoji.host}`, emoji);
} }

View File

@ -1,22 +1,22 @@
/* eslint-disable key-spacing */ /* eslint-disable key-spacing */
import { emojiRegex } from './emoji-regex.js'; import { emojiRegex } from "./emoji-regex.js";
import { fetchMeta } from './fetch-meta.js'; import { fetchMeta } from "./fetch-meta.js";
import { Emojis } from '@/models/index.js'; import { Emojis } from "@/models/index.js";
import { toPunyNullable } from './convert-host.js'; import { toPunyNullable } from "./convert-host.js";
import { IsNull } from 'typeorm'; import { IsNull } from "typeorm";
const legacies: Record<string, string> = { const legacies: Record<string, string> = {
'like': '👍', like: "👍",
'love': '❤️', // ここに記述する場合は異体字セレクタを入れない <- not that good because modern browsers just display it as the red heart so just convert it to it to not end up with two seperate reactions of "the same emoji" for the user love: "❤️", // ここに記述する場合は異体字セレクタを入れない <- not that good because modern browsers just display it as the red heart so just convert it to it to not end up with two seperate reactions of "the same emoji" for the user
'laugh': '😆', laugh: "😆",
'hmm': '🤔', hmm: "🤔",
'surprise': '😮', surprise: "😮",
'congrats': '🎉', congrats: "🎉",
'angry': '💢', angry: "💢",
'confused': '😥', confused: "😥",
'rip': '😇', rip: "😇",
'pudding': '🍮', pudding: "🍮",
'star': '⭐', star: "⭐",
}; };
export async function getFallbackReaction(): Promise<string> { export async function getFallbackReaction(): Promise<string> {
@ -54,7 +54,10 @@ export function convertLegacyReactions(reactions: Record<string, number>) {
return _reactions2; return _reactions2;
} }
export async function toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> { export async function toDbReaction(
reaction?: string | null,
reacterHost?: string | null,
): Promise<string> {
if (reaction == null) return await getFallbackReaction(); if (reaction == null) return await getFallbackReaction();
reacterHost = toPunyNullable(reacterHost); reacterHost = toPunyNullable(reacterHost);
@ -111,7 +114,7 @@ export function decodeReaction(str: string): DecodedReaction {
const host = custom[2] || null; const host = custom[2] || null;
return { return {
reaction: `:${name}@${host || '.'}:`, // ローカル分は@以降を省略するのではなく.にする reaction: `:${name}@${host || "."}:`, // ローカル分は@以降を省略するのではなく.にする
name, name,
host, host,
}; };

View File

@ -6,29 +6,29 @@ import {
packedMeDetailedSchema, packedMeDetailedSchema,
packedUserDetailedSchema, packedUserDetailedSchema,
packedUserSchema, packedUserSchema,
} from '@/models/schema/user.js'; } from "@/models/schema/user.js";
import { packedNoteSchema } from '@/models/schema/note.js'; import { packedNoteSchema } from "@/models/schema/note.js";
import { packedUserListSchema } from '@/models/schema/user-list.js'; import { packedUserListSchema } from "@/models/schema/user-list.js";
import { packedAppSchema } from '@/models/schema/app.js'; import { packedAppSchema } from "@/models/schema/app.js";
import { packedMessagingMessageSchema } from '@/models/schema/messaging-message.js'; import { packedMessagingMessageSchema } from "@/models/schema/messaging-message.js";
import { packedNotificationSchema } from '@/models/schema/notification.js'; import { packedNotificationSchema } from "@/models/schema/notification.js";
import { packedDriveFileSchema } from '@/models/schema/drive-file.js'; import { packedDriveFileSchema } from "@/models/schema/drive-file.js";
import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js'; import { packedDriveFolderSchema } from "@/models/schema/drive-folder.js";
import { packedFollowingSchema } from '@/models/schema/following.js'; import { packedFollowingSchema } from "@/models/schema/following.js";
import { packedMutingSchema } from '@/models/schema/muting.js'; import { packedMutingSchema } from "@/models/schema/muting.js";
import { packedBlockingSchema } from '@/models/schema/blocking.js'; import { packedBlockingSchema } from "@/models/schema/blocking.js";
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js'; import { packedNoteReactionSchema } from "@/models/schema/note-reaction.js";
import { packedHashtagSchema } from '@/models/schema/hashtag.js'; import { packedHashtagSchema } from "@/models/schema/hashtag.js";
import { packedPageSchema } from '@/models/schema/page.js'; import { packedPageSchema } from "@/models/schema/page.js";
import { packedUserGroupSchema } from '@/models/schema/user-group.js'; import { packedUserGroupSchema } from "@/models/schema/user-group.js";
import { packedNoteFavoriteSchema } from '@/models/schema/note-favorite.js'; import { packedNoteFavoriteSchema } from "@/models/schema/note-favorite.js";
import { packedChannelSchema } from '@/models/schema/channel.js'; import { packedChannelSchema } from "@/models/schema/channel.js";
import { packedAntennaSchema } from '@/models/schema/antenna.js'; import { packedAntennaSchema } from "@/models/schema/antenna.js";
import { packedClipSchema } from '@/models/schema/clip.js'; import { packedClipSchema } from "@/models/schema/clip.js";
import { packedFederationInstanceSchema } from '@/models/schema/federation-instance.js'; import { packedFederationInstanceSchema } from "@/models/schema/federation-instance.js";
import { packedQueueCountSchema } from '@/models/schema/queue.js'; import { packedQueueCountSchema } from "@/models/schema/queue.js";
import { packedGalleryPostSchema } from '@/models/schema/gallery-post.js'; import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
import { packedEmojiSchema } from '@/models/schema/emoji.js'; import { packedEmojiSchema } from "@/models/schema/emoji.js";
export const refs = { export const refs = {
UserLite: packedUserLiteSchema, UserLite: packedUserLiteSchema,
@ -65,23 +65,37 @@ export const refs = {
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>; export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
type TypeStringef = 'null' | 'boolean' | 'integer' | 'number' | 'string' | 'array' | 'object' | 'any'; type TypeStringef =
type StringDefToType<T extends TypeStringef> = | "null"
T extends 'null' ? null : | "boolean"
T extends 'boolean' ? boolean : | "integer"
T extends 'integer' ? number : | "number"
T extends 'number' ? number : | "string"
T extends 'string' ? string | Date : | "array"
T extends 'array' ? ReadonlyArray<any> : | "object"
T extends 'object' ? Record<string, any> : | "any";
any; type StringDefToType<T extends TypeStringef> = T extends "null"
? null
: T extends "boolean"
? boolean
: T extends "integer"
? number
: T extends "number"
? number
: T extends "string"
? string | Date
: T extends "array"
? ReadonlyArray<any>
: T extends "object"
? Record<string, any>
: any;
// https://swagger.io/specification/?sbsearch=optional#schema-object // https://swagger.io/specification/?sbsearch=optional#schema-object
type OfSchema = { type OfSchema = {
readonly anyOf?: ReadonlyArray<Schema>; readonly anyOf?: ReadonlyArray<Schema>;
readonly oneOf?: ReadonlyArray<Schema>; readonly oneOf?: ReadonlyArray<Schema>;
readonly allOf?: ReadonlyArray<Schema>; readonly allOf?: ReadonlyArray<Schema>;
} };
export interface Schema extends OfSchema { export interface Schema extends OfSchema {
readonly type?: TypeStringef; readonly type?: TypeStringef;
@ -89,13 +103,17 @@ export interface Schema extends OfSchema {
readonly optional?: boolean; readonly optional?: boolean;
readonly items?: Schema; readonly items?: Schema;
readonly properties?: Obj; readonly properties?: Obj;
readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>; readonly required?: ReadonlyArray<
Extract<keyof NonNullable<this["properties"]>, string>
>;
readonly description?: string; readonly description?: string;
readonly example?: any; readonly example?: any;
readonly format?: string; readonly format?: string;
readonly ref?: keyof typeof refs; readonly ref?: keyof typeof refs;
readonly enum?: ReadonlyArray<string>; readonly enum?: ReadonlyArray<string>;
readonly default?: (this['type'] extends TypeStringef ? StringDefToType<this['type']> : any) | null; readonly default?:
| (this["type"] extends TypeStringef ? StringDefToType<this["type"]> : any)
| null;
readonly maxLength?: number; readonly maxLength?: number;
readonly minLength?: number; readonly minLength?: number;
readonly maximum?: number; readonly maximum?: number;
@ -104,12 +122,18 @@ export interface Schema extends OfSchema {
} }
type RequiredPropertyNames<s extends Obj> = { type RequiredPropertyNames<s extends Obj> = {
[K in keyof s]: [K in keyof s]: // K is not optional
// K is not optional s[K]["optional"] extends false
s[K]['optional'] extends false ? K : ? K
// K has default value : // K has default value
s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K : s[K]["default"] extends
never | null
| string
| number
| boolean
| Record<string, unknown>
? K
: never;
}[keyof s]; }[keyof s];
export type Obj = Record<string, Schema>; export type Obj = Record<string, Schema>;
@ -117,56 +141,80 @@ export type Obj = Record<string, Schema>;
// https://github.com/misskey-dev/misskey/issues/8535 // https://github.com/misskey-dev/misskey/issues/8535
// To avoid excessive stack depth error, // To avoid excessive stack depth error,
// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it). // deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
export type ObjType<s extends Obj, RequiredProps extends keyof s> = export type ObjType<
UnionToIntersection< s extends Obj,
{ -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]> } & RequiredProps extends keyof s,
{ -readonly [R in RequiredProps]-?: SchemaType<s[R]> } & > = UnionToIntersection<
{ -readonly [P in keyof s]?: SchemaType<s[P]> } {
>; -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]>;
} & {
-readonly [R in RequiredProps]-?: SchemaType<s[R]>;
} & {
-readonly [P in keyof s]?: SchemaType<s[P]>;
}
>;
type NullOrUndefined<p extends Schema, T> = type NullOrUndefined<p extends Schema, T> =
| (p['nullable'] extends true ? null : never) | (p["nullable"] extends true ? null : never)
| (p['optional'] extends true ? undefined : never) | (p["optional"] extends true ? undefined : never)
| T; | T;
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
// Get intersection from union // Get intersection from union
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 // https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
// To get union, we use `Foo extends any ? Hoge<Foo> : never` // To get union, we use `Foo extends any ? Hoge<Foo> : never`
type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never; type UnionSchemaType<
type ArrayUnion<T> = T extends any ? Array<T> : never; a extends readonly any[],
X extends Schema = a[number],
> = X extends any ? SchemaType<X> : never;
type ArrayUnion<T> = T extends any ? Array<T> : never;
export type SchemaTypeDef<p extends Schema> = export type SchemaTypeDef<p extends Schema> = p["type"] extends "null"
p['type'] extends 'null' ? null : ? null
p['type'] extends 'integer' ? number : : p["type"] extends "integer"
p['type'] extends 'number' ? number : ? number
p['type'] extends 'string' ? ( : p["type"] extends "number"
p['enum'] extends readonly string[] ? ? number
p['enum'][number] : : p["type"] extends "string"
p['format'] extends 'date-time' ? string : // Dateにする ? p["enum"] extends readonly string[]
string ? p["enum"][number]
) : : p["format"] extends "date-time"
p['type'] extends 'boolean' ? boolean : ? string
p['type'] extends 'object' ? ( : // Dateにする
p['ref'] extends keyof typeof refs ? Packed<p['ref']> : string
p['properties'] extends NonNullable<Obj> ? ObjType<p['properties'], NonNullable<p['required']>[number]> : : p["type"] extends "boolean"
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & Partial<UnionToIntersection<UnionSchemaType<p['anyOf']>>> : ? boolean
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> : : p["type"] extends "object"
any ? p["ref"] extends keyof typeof refs
) : ? Packed<p["ref"]>
p['type'] extends 'array' ? ( : p["properties"] extends NonNullable<Obj>
p['items'] extends OfSchema ? ( ? ObjType<p["properties"], NonNullable<p["required"]>[number]>
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] : : p["anyOf"] extends ReadonlyArray<Schema>
p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> : ? UnionSchemaType<p["anyOf"]> &
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] : Partial<UnionToIntersection<UnionSchemaType<p["anyOf"]>>>
never : p["allOf"] extends ReadonlyArray<Schema>
) : ? UnionToIntersection<UnionSchemaType<p["allOf"]>>
p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] : : any
any[] : p["type"] extends "array"
) : ? p["items"] extends OfSchema
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> : ? p["items"]["anyOf"] extends ReadonlyArray<Schema>
any; ? UnionSchemaType<NonNullable<p["items"]["anyOf"]>>[]
: p["items"]["oneOf"] extends ReadonlyArray<Schema>
? ArrayUnion<UnionSchemaType<NonNullable<p["items"]["oneOf"]>>>
: p["items"]["allOf"] extends ReadonlyArray<Schema>
? UnionToIntersection<UnionSchemaType<NonNullable<p["items"]["allOf"]>>>[]
: never
: p["items"] extends NonNullable<Schema>
? SchemaTypeDef<p["items"]>[]
: any[]
: p["oneOf"] extends ReadonlyArray<Schema>
? UnionSchemaType<p["oneOf"]>
: any;
export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>; export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;

View File

@ -1,16 +1,19 @@
import * as crypto from 'node:crypto'; import * as crypto from "node:crypto";
const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; const L_CHARS = "0123456789abcdefghijklmnopqrstuvwxyz";
const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; const LU_CHARS =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
export function secureRndstr(length = 32, useLU = true): string { export function secureRndstr(length = 32, useLU = true): string {
const chars = useLU ? LU_CHARS : L_CHARS; const chars = useLU ? LU_CHARS : L_CHARS;
const chars_len = chars.length; const chars_len = chars.length;
let str = ''; let str = "";
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
let rand = Math.floor((crypto.randomBytes(1).readUInt8(0) / 0xFF) * chars_len); let rand = Math.floor(
(crypto.randomBytes(1).readUInt8(0) / 0xff) * chars_len,
);
if (rand === chars_len) { if (rand === chars_len) {
rand = chars_len - 1; rand = chars_len - 1;
} }

View File

@ -1,6 +1,6 @@
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from "@/misc/fetch-meta.js";
import { Instance } from '@/models/entities/instance.js'; import type { Instance } from "@/models/entities/instance.js";
import { Meta } from '@/models/entities/meta.js'; import type { Meta } from "@/models/entities/meta.js";
/** /**
* Returns whether a specific host (punycoded) should be blocked. * Returns whether a specific host (punycoded) should be blocked.
@ -9,7 +9,12 @@ import { Meta } from '@/models/entities/meta.js';
* @param meta a resolved Meta table * @param meta a resolved Meta table
* @returns whether the given host should be blocked * @returns whether the given host should be blocked
*/ */
export async function shouldBlockInstance(host: Instance['host'], meta?: Meta): Promise<boolean> { export async function shouldBlockInstance(
const { blockedHosts } = meta ?? await fetchMeta(); host: Instance["host"],
return blockedHosts.some(blockedHost => host === blockedHost || host.endsWith('.' + blockedHost)); meta?: Meta,
): Promise<boolean> {
const { blockedHosts } = meta ?? (await fetchMeta());
return blockedHosts.some(
(blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`),
);
} }

View File

@ -1,13 +1,17 @@
import * as os from 'node:os'; import * as os from "node:os";
import sysUtils from 'systeminformation'; import sysUtils from "systeminformation";
import Logger from '@/services/logger.js'; import type Logger from "@/services/logger.js";
export async function showMachineInfo(parentLogger: Logger) { export async function showMachineInfo(parentLogger: Logger) {
const logger = parentLogger.createSubLogger('machine'); const logger = parentLogger.createSubLogger("machine");
logger.debug(`Hostname: ${os.hostname()}`); logger.debug(`Hostname: ${os.hostname()}`);
logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`); logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`);
const mem = await sysUtils.mem(); const mem = await sysUtils.mem();
const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1); const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1); const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
logger.debug(`CPU: ${os.cpus().length} core MEM: ${totalmem}GB (available: ${availmem}GB)`); logger.debug(
`CPU: ${
os.cpus().length
} core MEM: ${totalmem}GB (available: ${availmem}GB)`,
);
} }

View File

@ -1,9 +1,9 @@
import { Brackets } from 'typeorm'; import { Brackets } from "typeorm";
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from "@/misc/fetch-meta.js";
import { Instances } from '@/models/index.js'; import { Instances } from "@/models/index.js";
import type { Instance } from '@/models/entities/instance.js'; import type { Instance } from "@/models/entities/instance.js";
import { DAY } from '@/const.js'; import { DAY } from "@/const.js";
import { shouldBlockInstance } from './should-block-instance.js'; import { shouldBlockInstance } from "./should-block-instance.js";
// Threshold from last contact after which an instance will be considered // Threshold from last contact after which an instance will be considered
// "dead" and should no longer get activities delivered to it. // "dead" and should no longer get activities delivered to it.
@ -11,32 +11,38 @@ const deadThreshold = 7 * DAY;
/** /**
* Returns the subset of hosts which should be skipped. * Returns the subset of hosts which should be skipped.
* *
* @param hosts array of punycoded instance hosts * @param hosts array of punycoded instance hosts
* @returns array of punycoed instance hosts that should be skipped (subset of hosts parameter) * @returns array of punycoed instance hosts that should be skipped (subset of hosts parameter)
*/ */
export async function skippedInstances(hosts: Instance['host'][]): Promise<Instance['host'][]> { export async function skippedInstances(
hosts: Instance["host"][],
): Promise<Instance["host"][]> {
// first check for blocked instances since that info may already be in memory // first check for blocked instances since that info may already be in memory
const meta = await fetchMeta(); const meta = await fetchMeta();
const shouldSkip = await Promise.all(hosts.map(host => shouldBlockInstance(host, meta))); const shouldSkip = await Promise.all(
hosts.map((host) => shouldBlockInstance(host, meta)),
);
const skipped = hosts.filter((_, i) => shouldSkip[i]); const skipped = hosts.filter((_, i) => shouldSkip[i]);
// if possible return early and skip accessing the database // if possible return early and skip accessing the database
if (skipped.length === hosts.length) return hosts; if (skipped.length === hosts.length) return hosts;
const deadTime = new Date(Date.now() - deadThreshold); const deadTime = new Date(Date.now() - deadThreshold);
return skipped.concat( return skipped.concat(
await Instances.createQueryBuilder('instance') await Instances.createQueryBuilder("instance")
.where('instance.host in (:...hosts)', { .where("instance.host in (:...hosts)", {
// don't check hosts again that we already know are suspended // don't check hosts again that we already know are suspended
// also avoids adding duplicates to the list // also avoids adding duplicates to the list
hosts: hosts.filter(host => !skipped.includes(host)), hosts: hosts.filter((host) => !skipped.includes(host)),
}) })
.andWhere(new Brackets(qb => { qb .andWhere(
.where('instance.isSuspended'); new Brackets((qb) => {
})) qb.where("instance.isSuspended");
.select('host') }),
)
.select("host")
.getRawMany(), .getRawMany(),
); );
} }
@ -49,7 +55,9 @@ export async function skippedInstances(hosts: Instance['host'][]): Promise<Insta
* @param host punycoded instance host * @param host punycoded instance host
* @returns whether the given host should be skipped * @returns whether the given host should be skipped
*/ */
export async function shouldSkipInstance(host: Instance['host']): Promise<boolean> { export async function shouldSkipInstance(
host: Instance["host"],
): Promise<boolean> {
const skipped = await skippedInstances([host]); const skipped = await skippedInstances([host]);
return skipped.length > 0; return skipped.length > 0;
} }

View File

@ -1,8 +1,14 @@
import { substring } from 'stringz'; import { substring } from "stringz";
export function truncate(input: string, size: number): string; export function truncate(input: string, size: number): string;
export function truncate(input: string | undefined, size: number): string | undefined; export function truncate(
export function truncate(input: string | undefined, size: number): string | undefined { input: string | undefined,
size: number,
): string | undefined;
export function truncate(
input: string | undefined,
size: number,
): string | undefined {
if (!input) { if (!input) {
return input; return input;
} else { } else {

View File

@ -1,6 +1,6 @@
import { Webhooks } from '@/models/index.js'; import { Webhooks } from "@/models/index.js";
import { Webhook } from '@/models/entities/webhook.js'; import type { Webhook } from "@/models/entities/webhook.js";
import { subscriber } from '@/db/redis.js'; import { subscriber } from "@/db/redis.js";
let webhooksFetched = false; let webhooksFetched = false;
let webhooks: Webhook[] = []; let webhooks: Webhook[] = [];
@ -16,31 +16,31 @@ export async function getActiveWebhooks() {
return webhooks; return webhooks;
} }
subscriber.on('message', async (_, data) => { subscriber.on("message", async (_, data) => {
const obj = JSON.parse(data); const obj = JSON.parse(data);
if (obj.channel === 'internal') { if (obj.channel === "internal") {
const { type, body } = obj.message; const { type, body } = obj.message;
switch (type) { switch (type) {
case 'webhookCreated': case "webhookCreated":
if (body.active) { if (body.active) {
webhooks.push(body); webhooks.push(body);
} }
break; break;
case 'webhookUpdated': case "webhookUpdated":
if (body.active) { if (body.active) {
const i = webhooks.findIndex(a => a.id === body.id); const i = webhooks.findIndex((a) => a.id === body.id);
if (i > -1) { if (i > -1) {
webhooks[i] = body; webhooks[i] = body;
} else { } else {
webhooks.push(body); webhooks.push(body);
} }
} else { } else {
webhooks = webhooks.filter(a => a.id !== body.id); webhooks = webhooks.filter((a) => a.id !== body.id);
} }
break; break;
case 'webhookDeleted': case "webhookDeleted":
webhooks = webhooks.filter(a => a.id !== body.id); webhooks = webhooks.filter((a) => a.id !== body.id);
break; break;
default: default:
break; break;

View File

@ -1,6 +1,13 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; import {
import { User } from './user.js'; PrimaryColumn,
import { id } from '../id.js'; Entity,
Index,
JoinColumn,
Column,
ManyToOne,
} from "typeorm";
import { User } from "./user.js";
import { id } from "../id.js";
@Entity() @Entity()
export class AbuseUserReport { export class AbuseUserReport {
@ -15,7 +22,7 @@ export class AbuseUserReport {
@Index() @Index()
@Column(id()) @Column(id())
public targetUserId: User['id']; public targetUserId: User["id"];
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -25,7 +32,7 @@ export class AbuseUserReport {
@Index() @Index()
@Column(id()) @Column(id())
public reporterId: User['id']; public reporterId: User["id"];
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -37,7 +44,7 @@ export class AbuseUserReport {
...id(), ...id(),
nullable: true, nullable: true,
}) })
public assigneeId: User['id'] | null; public assigneeId: User["id"] | null;
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'SET NULL', onDelete: 'SET NULL',

View File

@ -1,7 +1,14 @@
import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; import {
import { User } from './user.js'; Entity,
import { App } from './app.js'; PrimaryColumn,
import { id } from '../id.js'; Index,
Column,
ManyToOne,
JoinColumn,
} from "typeorm";
import { User } from "./user.js";
import { App } from "./app.js";
import { id } from "../id.js";
@Entity() @Entity()
export class AccessToken { export class AccessToken {
@ -39,7 +46,7 @@ export class AccessToken {
@Index() @Index()
@Column(id()) @Column(id())
public userId: User['id']; public userId: User["id"];
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -51,7 +58,7 @@ export class AccessToken {
...id(), ...id(),
nullable: true, nullable: true,
}) })
public appId: App['id'] | null; public appId: App["id"] | null;
@ManyToOne(type => App, { @ManyToOne(type => App, {
onDelete: 'CASCADE', onDelete: 'CASCADE',

View File

@ -1,5 +1,5 @@
import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; import { Entity, Index, Column, PrimaryColumn } from "typeorm";
import { id } from '../id.js'; import { id } from "../id.js";
@Entity() @Entity()
export class Ad { export class Ad {

View File

@ -1,7 +1,14 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; import {
import { User } from './user.js'; PrimaryColumn,
import { Announcement } from './announcement.js'; Entity,
import { id } from '../id.js'; Index,
JoinColumn,
Column,
ManyToOne,
} from "typeorm";
import { User } from "./user.js";
import { Announcement } from "./announcement.js";
import { id } from "../id.js";
@Entity() @Entity()
@Index(['userId', 'announcementId'], { unique: true }) @Index(['userId', 'announcementId'], { unique: true })
@ -16,7 +23,7 @@ export class AnnouncementRead {
@Index() @Index()
@Column(id()) @Column(id())
public userId: User['id']; public userId: User["id"];
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -26,7 +33,7 @@ export class AnnouncementRead {
@Index() @Index()
@Column(id()) @Column(id())
public announcementId: Announcement['id']; public announcementId: Announcement["id"];
@ManyToOne(type => Announcement, { @ManyToOne(type => Announcement, {
onDelete: 'CASCADE', onDelete: 'CASCADE',

View File

@ -1,5 +1,5 @@
import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; import { Entity, Index, Column, PrimaryColumn } from "typeorm";
import { id } from '../id.js'; import { id } from "../id.js";
@Entity() @Entity()
export class Announcement { export class Announcement {

View File

@ -1,7 +1,14 @@
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; import {
import { Note } from './note.js'; Entity,
import { Antenna } from './antenna.js'; Index,
import { id } from '../id.js'; JoinColumn,
Column,
ManyToOne,
PrimaryColumn,
} from "typeorm";
import { Note } from "./note.js";
import { Antenna } from "./antenna.js";
import { id } from "../id.js";
@Entity() @Entity()
@Index(['noteId', 'antennaId'], { unique: true }) @Index(['noteId', 'antennaId'], { unique: true })
@ -14,7 +21,7 @@ export class AntennaNote {
...id(), ...id(),
comment: 'The note ID.', comment: 'The note ID.',
}) })
public noteId: Note['id']; public noteId: Note["id"];
@ManyToOne(type => Note, { @ManyToOne(type => Note, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -27,7 +34,7 @@ export class AntennaNote {
...id(), ...id(),
comment: 'The antenna ID.', comment: 'The antenna ID.',
}) })
public antennaId: Antenna['id']; public antennaId: Antenna["id"];
@ManyToOne(type => Antenna, { @ManyToOne(type => Antenna, {
onDelete: 'CASCADE', onDelete: 'CASCADE',

View File

@ -1,8 +1,15 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; import {
import { User } from './user.js'; PrimaryColumn,
import { id } from '../id.js'; Entity,
import { UserList } from './user-list.js'; Index,
import { UserGroupJoining } from './user-group-joining.js'; JoinColumn,
Column,
ManyToOne,
} from "typeorm";
import { User } from "./user.js";
import { id } from "../id.js";
import { UserList } from "./user-list.js";
import { UserGroupJoining } from "./user-group-joining.js";
@Entity() @Entity()
export class Antenna { export class Antenna {
@ -19,7 +26,7 @@ export class Antenna {
...id(), ...id(),
comment: 'The owner ID.', comment: 'The owner ID.',
}) })
public userId: User['id']; public userId: User["id"];
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -34,13 +41,13 @@ export class Antenna {
public name: string; public name: string;
@Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] }) @Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] })
public src: 'home' | 'all' | 'users' | 'list' | 'group'; public src: "home" | "all" | "users" | "list" | "group";
@Column({ @Column({
...id(), ...id(),
nullable: true, nullable: true,
}) })
public userListId: UserList['id'] | null; public userListId: UserList["id"] | null;
@ManyToOne(type => UserList, { @ManyToOne(type => UserList, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -52,7 +59,7 @@ export class Antenna {
...id(), ...id(),
nullable: true, nullable: true,
}) })
public userGroupJoiningId: UserGroupJoining['id'] | null; public userGroupJoiningId: UserGroupJoining["id"] | null;
@ManyToOne(type => UserGroupJoining, { @ManyToOne(type => UserGroupJoining, {
onDelete: 'CASCADE', onDelete: 'CASCADE',

View File

@ -1,6 +1,6 @@
import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; import { Entity, PrimaryColumn, Column, Index, ManyToOne } from "typeorm";
import { User } from './user.js'; import { User } from "./user.js";
import { id } from '../id.js'; import { id } from "../id.js";
@Entity() @Entity()
export class App { export class App {
@ -19,7 +19,7 @@ export class App {
nullable: true, nullable: true,
comment: 'The owner ID.', comment: 'The owner ID.',
}) })
public userId: User['id'] | null; public userId: User["id"] | null;
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'SET NULL', onDelete: 'SET NULL',

View File

@ -1,6 +1,13 @@
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; import {
import { User } from './user.js'; PrimaryColumn,
import { id } from '../id.js'; Entity,
JoinColumn,
Column,
ManyToOne,
Index,
} from "typeorm";
import { User } from "./user.js";
import { id } from "../id.js";
@Entity() @Entity()
export class AttestationChallenge { export class AttestationChallenge {
@ -9,7 +16,7 @@ export class AttestationChallenge {
@Index() @Index()
@PrimaryColumn(id()) @PrimaryColumn(id())
public userId: User['id']; public userId: User["id"];
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',

View File

@ -1,7 +1,14 @@
import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; import {
import { User } from './user.js'; Entity,
import { App } from './app.js'; PrimaryColumn,
import { id } from '../id.js'; Index,
Column,
ManyToOne,
JoinColumn,
} from "typeorm";
import { User } from "./user.js";
import { App } from "./app.js";
import { id } from "../id.js";
@Entity() @Entity()
export class AuthSession { export class AuthSession {
@ -23,7 +30,7 @@ export class AuthSession {
...id(), ...id(),
nullable: true, nullable: true,
}) })
public userId: User['id'] | null; public userId: User["id"] | null;
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -33,7 +40,7 @@ export class AuthSession {
public user: User | null; public user: User | null;
@Column(id()) @Column(id())
public appId: App['id']; public appId: App["id"];
@ManyToOne(type => App, { @ManyToOne(type => App, {
onDelete: 'CASCADE', onDelete: 'CASCADE',

View File

@ -1,6 +1,13 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; import {
import { User } from './user.js'; PrimaryColumn,
import { id } from '../id.js'; Entity,
Index,
JoinColumn,
Column,
ManyToOne,
} from "typeorm";
import { User } from "./user.js";
import { id } from "../id.js";
@Entity() @Entity()
@Index(['blockerId', 'blockeeId'], { unique: true }) @Index(['blockerId', 'blockeeId'], { unique: true })
@ -19,7 +26,7 @@ export class Blocking {
...id(), ...id(),
comment: 'The blockee user ID.', comment: 'The blockee user ID.',
}) })
public blockeeId: User['id']; public blockeeId: User["id"];
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -32,7 +39,7 @@ export class Blocking {
...id(), ...id(),
comment: 'The blocker user ID.', comment: 'The blocker user ID.',
}) })
public blockerId: User['id']; public blockerId: User["id"];
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',

Some files were not shown because too many files have changed in this diff Show More