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:
commit
217a37a59a
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"eslint.packageManager": "pnpm",
|
"eslint.packageManager": "pnpm",
|
||||||
"workspace.workspaceFolderCheckCwd": false,
|
"workspace.workspaceFolderCheckCwd": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
declare module '*/package.json' {
|
declare module "*/package.json" {
|
||||||
interface IRepository {
|
interface IRepository {
|
||||||
type: string;
|
type: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import load from './load.js';
|
import load from "./load.js";
|
||||||
|
|
||||||
export default load();
|
export default load();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(/^\/(.+)\/(.*)$/);
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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]);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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})`);
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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)}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 += " (📊)";
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)];
|
||||||
|
|
|
@ -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()}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)];
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, "냥")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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<
|
||||||
|
a extends readonly any[],
|
||||||
|
X extends Schema = a[number],
|
||||||
|
> = X extends any ? SchemaType<X> : never;
|
||||||
type ArrayUnion<T> = T extends any ? Array<T> : 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>>;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -15,10 +15,14 @@ const deadThreshold = 7 * DAY;
|
||||||
* @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
|
||||||
|
@ -27,16 +31,18 @@ export async function skippedInstances(hosts: Instance['host'][]): Promise<Insta
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue