Merge tag '12.112.1' into develop

This commit is contained in:
2022-07-08 23:44:00 +09:00
573 changed files with 19694 additions and 14145 deletions

View File

@ -5,6 +5,6 @@
"loader=./test/loader.js"
],
"slow": 1000,
"timeout": 10000,
"timeout": 30000,
"exit": true
}

View File

@ -0,0 +1,5 @@
Font Awesome Icons
-------------------------
Ⓒ Font Awesome
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

View File

@ -0,0 +1,23 @@
export class nsfwDetection1655368940105 {
name = 'nsfwDetection1655368940105'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "drive_file" ADD "forceIsSensitive" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "drive_file" ADD "predictedIsSensitive" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."predictedIsSensitive" IS 'Whether the DriveFile is NSFW. (predict)'`);
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetection_enum" AS ENUM('none', 'all', 'local', 'remote')`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveImageDetection" "public"."meta_sensitiveimagedetection_enum" NOT NULL DEFAULT 'none'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "forceIsSensitiveWhenPredicted" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`CREATE INDEX "IDX_fc2d74a6d7d8b11292a851d8f8" ON "drive_file" ("predictedIsSensitive") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_fc2d74a6d7d8b11292a851d8f8"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "forceIsSensitiveWhenPredicted"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveImageDetection"`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetection_enum"`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."predictedIsSensitive" IS 'Whether the DriveFile is NSFW. (predict)'`);
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "predictedIsSensitive"`);
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "forceIsSensitive"`);
}
}

View File

@ -0,0 +1,15 @@
export class nsfwDetection21655371960534 {
name = 'nsfwDetection21655371960534'
async up(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" AS ENUM('medium', 'low', 'high')`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveImageDetectionSensitivity" "public"."meta_sensitiveimagedetectionsensitivity_enum" NOT NULL DEFAULT 'medium'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "disallowUploadWhenPredictedAsPorn" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disallowUploadWhenPredictedAsPorn"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveImageDetectionSensitivity"`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum"`);
}
}

View File

@ -0,0 +1,21 @@
export class nsfwDetection31655388169582 {
name = 'nsfwDetection31655388169582'
async up(queryRunner) {
await queryRunner.query(`ALTER TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" RENAME TO "meta_sensitiveimagedetectionsensitivity_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" AS ENUM('medium', 'low', 'high', 'veryLow', 'veryHigh')`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" USING "sensitiveImageDetectionSensitivity"::"text"::"public"."meta_sensitiveimagedetectionsensitivity_enum"`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" SET DEFAULT 'medium'`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum_old"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum_old" AS ENUM('medium', 'low', 'high')`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum_old" USING "sensitiveImageDetectionSensitivity"::"text"::"public"."meta_sensitiveimagedetectionsensitivity_enum_old"`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "sensitiveImageDetectionSensitivity" SET DEFAULT 'medium'`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum"`);
await queryRunner.query(`ALTER TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum_old" RENAME TO "meta_sensitiveimagedetectionsensitivity_enum"`);
}
}

View File

@ -0,0 +1,25 @@
export class nsfwDetection41655393015659 {
name = 'nsfwDetection41655393015659'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveImageDetection"`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetection_enum"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveImageDetectionSensitivity"`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum"`);
await queryRunner.query(`CREATE TYPE "public"."meta_sensitivemediadetection_enum" AS ENUM('none', 'all', 'local', 'remote')`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetection" "public"."meta_sensitivemediadetection_enum" NOT NULL DEFAULT 'none'`);
await queryRunner.query(`CREATE TYPE "public"."meta_sensitivemediadetectionsensitivity_enum" AS ENUM('medium', 'low', 'high', 'veryLow', 'veryHigh')`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaDetectionSensitivity" "public"."meta_sensitivemediadetectionsensitivity_enum" NOT NULL DEFAULT 'medium'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetectionSensitivity"`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitivemediadetectionsensitivity_enum"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaDetection"`);
await queryRunner.query(`DROP TYPE "public"."meta_sensitivemediadetection_enum"`);
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetectionsensitivity_enum" AS ENUM('medium', 'low', 'high', 'veryLow', 'veryHigh')`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveImageDetectionSensitivity" "public"."meta_sensitiveimagedetectionsensitivity_enum" NOT NULL DEFAULT 'medium'`);
await queryRunner.query(`CREATE TYPE "public"."meta_sensitiveimagedetection_enum" AS ENUM('none', 'all', 'local', 'remote')`);
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveImageDetection" "public"."meta_sensitiveimagedetection_enum" NOT NULL DEFAULT 'none'`);
}
}

View File

@ -0,0 +1,13 @@
export class driveCapacityOverrideMb1655813815729 {
name = 'driveCapacityOverrideMb1655813815729'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "driveCapacityOverrideMb" integer`);
await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`);
}
async down(queryRunner) {
await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "driveCapacityOverrideMb"`);
}
}

View File

@ -0,0 +1,17 @@
export class userIp1655918165614 {
name = 'userIp1655918165614'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "user_ip" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "ip" character varying(128) NOT NULL, CONSTRAINT "PK_2c44ddfbf7c0464d028dcef325e" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_7f7f1c66f48e9a8e18a33bc515" ON "user_ip" ("userId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_361b500e06721013c124b7b6c5" ON "user_ip" ("userId", "ip") `);
await queryRunner.query(`ALTER TABLE "user_ip" ADD CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_ip" DROP CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150"`);
await queryRunner.query(`DROP INDEX "public"."IDX_361b500e06721013c124b7b6c5"`);
await queryRunner.query(`DROP INDEX "public"."IDX_7f7f1c66f48e9a8e18a33bc515"`);
await queryRunner.query(`DROP TABLE "user_ip"`);
}
}

View File

@ -0,0 +1,13 @@
export class fileIp1656122560740 {
name = 'fileIp1656122560740'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "drive_file" ADD "requestHeaders" jsonb DEFAULT '{}'`);
await queryRunner.query(`ALTER TABLE "drive_file" ADD "requestIp" character varying(128)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "requestIp"`);
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "requestHeaders"`);
}
}

View File

@ -0,0 +1,33 @@
export class nsfwDetection51656251734807 {
name = 'nsfwDetection51656251734807'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_fc2d74a6d7d8b11292a851d8f8"`);
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "forceIsSensitive"`);
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "predictedIsSensitive"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "forceIsSensitiveWhenPredicted"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disallowUploadWhenPredictedAsPorn"`);
await queryRunner.query(`ALTER TABLE "drive_file" ADD "maybeSensitive" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."maybeSensitive" IS 'Whether the DriveFile is NSFW. (predict)'`);
await queryRunner.query(`ALTER TABLE "drive_file" ADD "maybePorn" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "setSensitiveFlagAutomatically" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "autoSensitive" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`CREATE INDEX "IDX_3b33dff77bb64b23c88151d23e" ON "drive_file" ("maybeSensitive") `);
await queryRunner.query(`CREATE INDEX "IDX_8bdcd3dd2bddb78014999a16ce" ON "drive_file" ("maybePorn") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_8bdcd3dd2bddb78014999a16ce"`);
await queryRunner.query(`DROP INDEX "public"."IDX_3b33dff77bb64b23c88151d23e"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "autoSensitive"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "setSensitiveFlagAutomatically"`);
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "maybePorn"`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."maybeSensitive" IS 'Whether the DriveFile is NSFW. (predict)'`);
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "maybeSensitive"`);
await queryRunner.query(`ALTER TABLE "meta" ADD "disallowUploadWhenPredictedAsPorn" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "forceIsSensitiveWhenPredicted" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`ALTER TABLE "drive_file" ADD "predictedIsSensitive" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "drive_file" ADD "forceIsSensitive" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`CREATE INDEX "IDX_fc2d74a6d7d8b11292a851d8f8" ON "drive_file" ("predictedIsSensitive") `);
}
}

View File

@ -0,0 +1,13 @@
export class ip21656328812281 {
name = 'ip21656328812281'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_ip" DROP CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150"`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableIpLogging" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIpLogging"`);
await queryRunner.query(`ALTER TABLE "user_ip" ADD CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
}

View File

@ -0,0 +1,11 @@
export class nsfwDetection61656408772602 {
name = 'nsfwDetection61656408772602'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableSensitiveMediaDetectionForVideos" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableSensitiveMediaDetectionForVideos"`);
}
}

View File

@ -0,0 +1,11 @@
export class userModerationNote1656772790599 {
name = 'userModerationNote1656772790599'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "moderationNote"`);
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,7 +14,7 @@
"lodash": "^4.17.21"
},
"dependencies": {
"@bull-board/koa": "3.11.1",
"@bull-board/koa": "4.0.0",
"@discordapp/twemoji": "14.0.2",
"@elastic/elasticsearch": "7.11.0",
"@koa/cors": "3.1.0",
@ -23,19 +23,21 @@
"@peertube/http-signature": "1.6.0",
"@sinonjs/fake-timers": "9.1.2",
"@syuilo/aiscript": "0.11.1",
"@tensorflow/tfjs-node": "3.18.0",
"abort-controller": "3.0.0",
"ajv": "8.11.0",
"archiver": "5.3.1",
"autobind-decorator": "2.4.0",
"autwh": "0.1.0",
"aws-sdk": "2.1152.0",
"aws-sdk": "2.1165.0",
"bcryptjs": "2.4.3",
"blurhash": "1.1.5",
"bull": "4.8.3",
"bull": "4.8.4",
"cacheable-lookup": "6.0.4",
"cbor": "8.1.0",
"chalk": "5.0.1",
"chalk-template": "0.4.0",
"chokidar": "3.3.1",
"cli-highlight": "2.1.11",
"color-convert": "2.0.1",
"content-disposition": "0.5.4",
@ -47,14 +49,15 @@
"fluent-ffmpeg": "2.1.2",
"got": "12.1.0",
"hpagent": "0.1.2",
"ioredis": "4.28.5",
"ip-cidr": "3.0.10",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "19.0.0",
"jsdom": "20.0.0",
"json5": "2.2.1",
"json5-loader": "4.0.1",
"jsonld": "6.0.0",
"jsrsasign": "10.5.24",
"jsrsasign": "10.5.25",
"koa": "2.13.4",
"koa-bodyparser": "4.3.0",
"koa-favicon": "2.1.0",
@ -72,26 +75,27 @@
"multer": "1.4.4",
"nested-property": "4.0.0",
"node-fetch": "3.2.6",
"nodemailer": "6.7.5",
"nodemailer": "6.7.6",
"nsfwjs": "2.4.1",
"os-utils": "0.0.14",
"parse5": "6.0.1",
"parse5": "7.0.0",
"pg": "8.7.3",
"private-ip": "2.3.3",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
"punycode": "2.1.1",
"pureimage": "0.3.8",
"pureimage": "0.3.14",
"qrcode": "1.5.0",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.17.4",
"redis": "3.1.2",
"re2": "1.17.7",
"redis-lock": "0.1.4",
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
"require-all": "3.0.0",
"rndstr": "1.0.0",
"rss-parser": "3.12.0",
"s-age": "1.1.2",
"sanitize-html": "2.7.0",
"semver": "7.3.7",
@ -100,17 +104,17 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"style-loader": "3.3.1",
"summaly": "2.5.1",
"summaly": "2.6.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.11.16",
"systeminformation": "5.11.22",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
"ts-loader": "9.3.0",
"ts-loader": "9.3.1",
"ts-node": "10.8.1",
"tsc-alias": "1.6.9",
"tsc-alias": "1.6.11",
"tsconfig-paths": "4.0.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.6",
"typeorm": "0.3.7",
"ulid": "2.3.0",
"unzipper": "0.10.11",
"uuid": "8.3.2",
@ -121,7 +125,6 @@
},
"devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.97",
"@types/semver": "7.3.9",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.8",
"@types/cbor": "6.0.0",
@ -144,11 +147,10 @@
"@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11",
"@types/mocha": "9.1.1",
"@types/node": "17.0.41",
"@types/node": "18.0.0",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.4",
"@types/oauth": "0.9.1",
"@types/parse5": "6.0.3",
"@types/pug": "2.0.6",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.4.2",
@ -157,7 +159,8 @@
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.6.2",
"@types/sharp": "0.30.2",
"@types/semver": "7.3.10",
"@types/sharp": "0.30.4",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7",
"@types/tinycolor2": "1.4.3",
@ -166,12 +169,12 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.27.1",
"@typescript-eslint/parser": "5.27.1",
"typescript": "4.7.3",
"eslint": "8.17.0",
"eslint-plugin-import": "2.26.0",
"@typescript-eslint/eslint-plugin": "5.30.0",
"@typescript-eslint/parser": "5.30.0",
"cross-env": "7.0.3",
"execa": "6.1.0"
"eslint": "8.18.0",
"eslint-plugin-import": "2.26.0",
"execa": "6.1.0",
"typescript": "4.7.4"
}
}

View File

@ -19,6 +19,7 @@ export type Source = {
redis: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;

View File

@ -68,9 +68,10 @@ import { RegistryItem } from '@/models/entities/registry-item.js';
import { Ad } from '@/models/entities/ad.js';
import { PasswordResetRequest } from '@/models/entities/password-reset-request.js';
import { UserPending } from '@/models/entities/user-pending.js';
import { Webhook } from '@/models/entities/webhook.js';
import { UserIp } from '@/models/entities/user-ip.js';
import { entities as charts } from '@/services/chart/entities.js';
import { Webhook } from '@/models/entities/webhook.js';
import { envOption } from '../env.js';
import { dbLogger } from './logger.js';
import { redisClient } from './redis.js';
@ -173,6 +174,7 @@ export const entities = [
PasswordResetRequest,
UserPending,
Webhook,
UserIp,
...charts,
];
@ -192,12 +194,13 @@ export const db = new DataSource({
synchronize: process.env.NODE_ENV === 'test',
dropSchema: process.env.NODE_ENV === 'test',
cache: !config.db.disableCache ? {
type: 'redis',
type: 'ioredis',
options: {
host: config.redis.host,
port: config.redis.port,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
prefix: `${config.redis.prefix}:query:`,
keyPrefix: `${config.redis.prefix}:query:`,
db: config.redis.db || 0,
},
} : false,
@ -226,7 +229,7 @@ export async function initDb(force = false) {
export async function resetDb() {
const reset = async () => {
await redisClient.FLUSHDB();
await redisClient.flushdb();
const tables = await db.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema')

View File

@ -1,16 +1,15 @@
import * as redis from 'redis';
import Redis from 'ioredis';
import config from '@/config/index.js';
export function createConnection() {
return redis.createClient(
config.redis.port,
config.redis.host,
{
password: config.redis.pass,
prefix: config.redis.prefix,
db: config.redis.db || 0,
}
);
return new Redis({
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db || 0,
});
}
export const subsdcriber = createConnection();

View File

@ -1,8 +1,10 @@
import * as parse5 from 'parse5';
import treeAdapter from 'parse5/lib/tree-adapters/default.js';
import { URL } from 'node:url';
import * as parse5 from 'parse5';
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const treeAdapter = TreeAdapter.defaultTreeAdapter;
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
export function fromHtml(html: string, hashtagNames?: string[]): string {
@ -19,7 +21,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
return text.trim();
function getText(node: parse5.Node): string {
function getText(node: TreeAdapter.Node): string {
if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n';
@ -31,7 +33,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
return '';
}
function appendChildren(childNodes: parse5.ChildNode[]): void {
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
if (childNodes) {
for (const n of childNodes) {
analyze(n);
@ -39,7 +41,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
}
}
function analyze(node: parse5.Node) {
function analyze(node: TreeAdapter.Node) {
if (treeAdapter.isTextNode(node)) {
text += node.value;
return;
@ -170,7 +172,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
const t = getText(node);
if (t) {
text += '\n> ';
text += t.split('\n').join(`\n> `);
text += t.split('\n').join('\n> ');
}
break;
}

View File

@ -16,11 +16,13 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
if (me && (note.userId === me.id)) return false;
if (mutedWords.length > 0) {
if (note.text == null) return false;
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
if (text === '') return false;
const matched = mutedWords.some(filter => {
if (Array.isArray(filter)) {
return filter.every(keyword => note.text!.includes(keyword));
return filter.every(keyword => text.includes(keyword));
} else {
// represents RegExp
const regexp = filter.match(/^\/(.+)\/(.*)$/);
@ -29,7 +31,7 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
if (!regexp) return false;
try {
return new RE2(regexp[1], regexp[2]).test(note.text!);
return new RE2(regexp[1], regexp[2]).test(text);
} catch (err) {
// This should never happen due to input sanitisation.
return false;

View File

@ -11,9 +11,14 @@ export function createTemp(): Promise<[string, () => void]> {
export function createTempDir(): Promise<[string, () => void]> {
return new Promise<[string, () => void]>((res, rej) => {
tmp.dir((e, path, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
tmp.dir(
{
unsafeCleanup: true,
},
(e, path, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
}
);
});
}

View File

@ -1,12 +1,18 @@
import * as fs from 'node:fs';
import * as crypto from 'node:crypto';
import { join } from 'node:path';
import * as stream from 'node:stream';
import * as util from 'node:util';
import { FSWatcher } from 'chokidar';
import { fileTypeFromFile } from 'file-type';
import FFmpeg from 'fluent-ffmpeg';
import isSvg from 'is-svg';
import probeImageSize from 'probe-image-size';
import { type predictionType } from 'nsfwjs';
import sharp from 'sharp';
import { encode } from 'blurhash';
import { detectSensitive } from '@/services/detect-sensitive.js';
import { createTempDir } from './create-temp.js';
const pipeline = util.promisify(stream.pipeline);
@ -21,6 +27,8 @@ export type FileInfo = {
height?: number;
orientation?: number;
blurhash?: string;
sensitive: boolean;
porn: boolean;
warnings: string[];
};
@ -37,7 +45,12 @@ const TYPE_SVG = {
/**
* Get file information
*/
export async function getFileInfo(path: string): Promise<FileInfo> {
export async function getFileInfo(path: string, opts: {
skipSensitiveDetection: boolean;
sensitiveThreshold?: number;
sensitiveThresholdForPorn?: number;
enableSensitiveMediaDetectionForVideos?: boolean;
}): Promise<FileInfo> {
const warnings = [] as string[];
const size = await getFileSize(path);
@ -58,7 +71,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
// うまく判定できない画像は octet-stream にする
if (!imageSize) {
warnings.push(`cannot detect image dimensions`);
warnings.push('cannot detect image dimensions');
type = TYPE_OCTET_STREAM;
} else if (imageSize.wUnits === 'px') {
width = imageSize.width;
@ -67,7 +80,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
// 制限を超えている画像は octet-stream にする
if (imageSize.width > 16383 || imageSize.height > 16383) {
warnings.push(`image dimensions exceeds limits`);
warnings.push('image dimensions exceeds limits');
type = TYPE_OCTET_STREAM;
}
} else {
@ -84,6 +97,19 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
});
}
let sensitive = false;
let porn = false;
if (!opts.skipSensitiveDetection) {
[sensitive, porn] = await detectSensitivity(
path,
type.mime,
opts.sensitiveThreshold ?? 0.5,
opts.sensitiveThresholdForPorn ?? 0.75,
opts.enableSensitiveMediaDetectionForVideos ?? false,
);
}
return {
size,
md5,
@ -92,10 +118,150 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
height,
orientation,
blurhash,
sensitive,
porn,
warnings,
};
}
async function detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> {
let sensitive = false;
let porn = false;
function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] {
let sensitive = false;
let porn = false;
if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > 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;
return [sensitive, porn];
}
if (['image/jpeg', 'image/png', 'image/webp'].includes(mime)) {
const result = await detectSensitive(source);
if (result) {
[sensitive, porn] = judgePrediction(result);
}
} else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
const [outDir, disposeOutDir] = await createTempDir();
try {
const command = FFmpeg()
.input(source)
.inputOptions([
'-skip_frame', 'nokey', // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない)
'-lowres', '3', // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない)
])
.noAudio()
.videoFilters([
{
filter: 'select', // フレームのフィルタリング
options: {
e: 'eq(pict_type,PICT_TYPE_I)', // I-Frame のみをフィルタするVP9 とかはデコードしてみないとわからないっぽい)
},
},
{
filter: 'blackframe', // 暗いフレームの検出
options: {
amount: '0', // 暗さに関わらず全てのフレームで測定値を取る
},
},
{
filter: 'metadata',
options: {
mode: 'select', // フレーム選択モード
key: 'lavfi.blackframe.pblack', // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する)
value: '50',
function: 'less', // 50% 未満のフレームを選択する50% 以上暗部があるフレームだと誤検知を招くかもしれないので)
},
},
{
filter: 'scale',
options: {
w: 299,
h: 299,
},
},
])
.format('image2')
.output(join(outDir, '%d.png'))
.outputOptions(['-vsync', '0']); // 可変フレームレートにすることで穴埋めをさせない
const results: ReturnType<typeof judgePrediction>[] = [];
let frameIndex = 0;
let targetIndex = 0;
let nextIndex = 1;
for await (const path of asyncIterateFrames(outDir, command)) {
try {
const index = frameIndex++;
if (index !== targetIndex) {
continue;
}
targetIndex = nextIndex;
nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける
const result = await detectSensitive(path);
if (result) {
results.push(judgePrediction(result));
}
} finally {
fs.promises.unlink(path);
}
}
sensitive = results.filter(x => x[0]).length >= Math.ceil(results.length * sensitiveThreshold);
porn = results.filter(x => x[1]).length >= Math.ceil(results.length * sensitiveThresholdForPorn);
} finally {
disposeOutDir();
}
}
return [sensitive, porn];
}
async function* asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
const watcher = new FSWatcher({
cwd,
disableGlobbing: true,
});
let finished = false;
command.once('end', () => {
finished = true;
watcher.close();
});
command.run();
for (let i = 1; true; i++) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition
const current = `${i}.png`;
const next = `${i + 1}.png`;
const framePath = join(cwd, current);
if (await exists(join(cwd, next))) {
yield framePath;
} else if (!finished) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition
watcher.add(next);
await new Promise<void>((resolve, reject) => {
watcher.on('add', function onAdd(path) {
if (path === next) { // 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている
watcher.unwatch(current);
watcher.off('add', onAdd);
resolve();
}
});
command.once('end', resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている
command.once('error', reject);
});
yield framePath;
} else if (await exists(framePath)) {
yield framePath;
} else {
return;
}
}
}
function exists(path: string): Promise<boolean> {
return fs.promises.access(path).then(() => true, () => false);
}
/**
* Detect MIME Type and extension
*/

View File

@ -1,15 +0,0 @@
export function isBlockerUserRelated(note: any, blockerUserIds: Set<string>): boolean {
if (blockerUserIds.has(note.userId)) {
return true;
}
if (note.reply != null && blockerUserIds.has(note.reply.userId)) {
return true;
}
if (note.renote != null && blockerUserIds.has(note.renote.userId)) {
return true;
}
return false;
}

View File

@ -0,0 +1,8 @@
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
const dictionary = {
'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'],
};
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);

View File

@ -1,15 +0,0 @@
export function isMutedUserRelated(note: any, mutedUserIds: Set<string>): boolean {
if (mutedUserIds.has(note.userId)) {
return true;
}
if (note.reply != null && mutedUserIds.has(note.reply.userId)) {
return true;
}
if (note.renote != null && mutedUserIds.has(note.renote.userId)) {
return true;
}
return false;
}

View File

@ -0,0 +1,15 @@
export function isUserRelated(note: any, userIds: Set<string>): boolean {
if (userIds.has(note.userId)) {
return true;
}
if (note.reply != null && userIds.has(note.reply.userId)) {
return true;
}
if (note.renote != null && userIds.has(note.renote.userId)) {
return true;
}
return false;
}

View File

@ -1,7 +1,7 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from '../id.js';
import { User } from './user.js';
import { DriveFolder } from './drive-folder.js';
import { id } from '../id.js';
@Entity()
@Index(['userId', 'folderId', 'id'])
@ -156,6 +156,19 @@ export class DriveFile {
})
public isSensitive: boolean;
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the DriveFile is NSFW. (predict)',
})
public maybeSensitive: boolean;
@Index()
@Column('boolean', {
default: false,
})
public maybePorn: boolean;
/**
* 外部の(信頼されていない)URLへの直リンクか否か
*/
@ -165,4 +178,15 @@ export class DriveFile {
comment: 'Whether the DriveFile is direct link to remote server.',
})
public isLink: boolean;
@Column('jsonb', {
default: {},
nullable: true,
})
public requestHeaders: Record<string, string> | null;
@Column('varchar', {
length: 128, nullable: true,
})
public requestIp: string | null;
}

View File

@ -1,6 +1,6 @@
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.js';
import { id } from '../id.js';
import { User } from './user.js';
import { Clip } from './clip.js';
@Entity()
@ -188,6 +188,28 @@ export class Meta {
})
public recaptchaSecretKey: string | null;
@Column('enum', {
enum: ['none', 'all', 'local', 'remote'],
default: 'none',
})
public sensitiveMediaDetection: 'none' | 'all' | 'local' | 'remote';
@Column('enum', {
enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'],
default: 'medium',
})
public sensitiveMediaDetectionSensitivity: 'medium' | 'low' | 'high' | 'veryLow' | 'veryHigh';
@Column('boolean', {
default: false,
})
public setSensitiveFlagAutomatically: boolean;
@Column('boolean', {
default: false,
})
public enableSensitiveMediaDetectionForVideos: boolean;
@Column('integer', {
default: 1024,
comment: 'Drive capacity of a local user (MB)',
@ -427,4 +449,9 @@ export class Meta {
default: true,
})
public objectStorageS3ForcePathStyle: boolean;
@Column('boolean', {
default: false,
})
public enableIpLogging: boolean;
}

View File

@ -0,0 +1,24 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { id } from '../id.js';
import { Note } from './note.js';
import { User } from './user.js';
@Entity()
@Index(['userId', 'ip'], { unique: true })
export class UserIp {
@PrimaryGeneratedColumn()
public id: string;
@Column('timestamp with time zone', {
})
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@Column('varchar', {
length: 128,
})
public ip: string;
}

View File

@ -1,8 +1,8 @@
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { ffVisibility, notificationTypes } from '@/types.js';
import { id } from '../id.js';
import { User } from './user.js';
import { Page } from './page.js';
import { ffVisibility, notificationTypes } from '@/types.js';
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
@ -117,6 +117,11 @@ export class UserProfile {
})
public password: string | null;
@Column('varchar', {
length: 8192, default: '',
})
public moderationNote: string | null;
// TODO: そのうち消す
@Column('jsonb', {
default: {},
@ -147,6 +152,11 @@ export class UserProfile {
})
public alwaysMarkNsfw: boolean;
@Column('boolean', {
default: false,
})
public autoSensitive: boolean;
@Column('boolean', {
default: false,
})

View File

@ -218,6 +218,12 @@ export class User {
})
public token: string | null;
@Column('integer', {
nullable: true,
comment: 'Overrides user drive capacity limit',
})
public driveCapacityOverrideMb: number | null;
constructor(data: Partial<User>) {
if (data == null) return;

View File

@ -65,6 +65,7 @@ import { PasswordResetRequest } from './entities/password-reset-request.js';
import { UserPending } from './entities/user-pending.js';
import { InstanceRepository } from './repositories/instance.js';
import { Webhook } from './entities/webhook.js';
import { UserIp } from './entities/user-ip.js';
export const Announcements = db.getRepository(Announcement);
export const AnnouncementReads = db.getRepository(AnnouncementRead);
@ -90,6 +91,7 @@ export const UserGroups = (UserGroupRepository);
export const UserGroupJoinings = db.getRepository(UserGroupJoining);
export const UserGroupInvitations = (UserGroupInvitationRepository);
export const UserNotePinings = db.getRepository(UserNotePining);
export const UserIps = db.getRepository(UserIp);
export const UsedUsernames = db.getRepository(UsedUsername);
export const Followings = (FollowingRepository);
export const FollowRequests = (FollowRequestRepository);

View File

@ -1,11 +1,13 @@
import { db } from '@/db/postgre.js';
import { Instance } from '@/models/entities/instance.js';
import { Packed } from '@/misc/schema.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
export const InstanceRepository = db.getRepository(Instance).extend({
async pack(
instance: Instance,
): Promise<Packed<'FederationInstance'>> {
const meta = await fetchMeta();
return {
id: instance.id,
caughtAt: instance.caughtAt.toISOString(),
@ -18,6 +20,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended,
isBlocked: meta.blockedHosts.includes(instance.host),
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
openRegistrations: instance.openRegistrations,
@ -26,6 +29,8 @@ export const InstanceRepository = db.getRepository(Instance).extend({
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
};
},

View File

@ -315,6 +315,7 @@ export const UserRepository = db.getRepository(User).extend({
} : undefined) : undefined,
emojis: populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
driveCapacityOverrideMb: user.driveCapacityOverrideMb,
...(opts.detail ? {
url: profile!.url,
@ -359,6 +360,7 @@ export const UserRepository = db.getRepository(User).extend({
injectFeaturedNote: profile!.injectFeaturedNote,
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
autoSensitive: profile!.autoSensitive,
carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed,
noCrawle: profile!.noCrawle,

View File

@ -52,6 +52,10 @@ export const packedFederationInstanceSchema = {
type: 'boolean',
optional: false, nullable: false,
},
isBlocked: {
type: 'boolean',
optional: false, nullable: false,
},
softwareName: {
type: 'string',
optional: false, nullable: true,
@ -88,6 +92,15 @@ export const packedFederationInstanceSchema = {
optional: false, nullable: true,
format: 'url',
},
faviconUrl: {
type: 'string',
optional: false, nullable: true,
format: 'url',
},
themeColor: {
type: 'string',
optional: false, nullable: true,
},
infoUpdatedAt: {
type: 'string',
optional: false, nullable: true,

View File

@ -161,19 +161,19 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'array',
nullable: false, optional: false,
items: {
type: 'object',
nullable: false, optional: false,
properties: {
name: {
type: 'string',
nullable: false, optional: false,
},
value: {
type: 'string',
nullable: false, optional: false,
},
type: 'object',
nullable: false, optional: false,
properties: {
name: {
type: 'string',
nullable: false, optional: false,
},
maxLength: 4,
value: {
type: 'string',
nullable: false, optional: false,
},
},
maxLength: 4,
},
},
followersCount: {
@ -292,6 +292,10 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: true, optional: false,
},
autoSensitive: {
type: 'boolean',
nullable: true, optional: false,
},
carefulBot: {
type: 'boolean',
nullable: true, optional: false,

View File

@ -2,6 +2,9 @@ import httpSignature from '@peertube/http-signature';
import { v4 as uuid } from 'uuid';
import config from '@/config/index.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { IActivity } from '@/remote/activitypub/type.js';
import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
import { envOption } from '../env.js';
import processDeliver from './processors/deliver.js';
@ -12,18 +15,15 @@ import processSystemQueue from './processors/system/index.js';
import processWebhookDeliver from './processors/webhook-deliver.js';
import { endedPollNotification } from './processors/ended-poll-notification.js';
import { queueLogger } from './logger.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { getJobInfo } from './get-job-info.js';
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js';
import { ThinUser } from './types.js';
import { IActivity } from '@/remote/activitypub/type.js';
import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
function renderError(e: Error): any {
return {
stack: e?.stack,
message: e?.message,
name: e?.name,
stack: e.stack,
message: e.message,
name: e.name,
};
}
@ -314,6 +314,12 @@ export default function() {
removeOnComplete: true,
});
systemQueue.add('clean', {
}, {
repeat: { cron: '0 0 * * *' },
removeOnComplete: true,
});
systemQueue.add('checkExpiredMutings', {
}, {
repeat: { cron: '*/5 * * * *' },

View File

@ -6,6 +6,7 @@ export function initialize<T>(name: string, limitPerSec = -1) {
redis: {
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
db: config.redis.db || 0,
},

View File

@ -0,0 +1,18 @@
import Bull from 'bull';
import { LessThan } from 'typeorm';
import { UserIps } from '@/models/index.js';
import { queueLogger } from '../../logger.js';
const logger = queueLogger.createSubLogger('clean');
export async function clean(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> {
logger.info('Cleaning...');
UserIps.delete({
createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
});
logger.succ('Cleaned.');
done();
}

View File

@ -3,12 +3,14 @@ import { tickCharts } from './tick-charts.js';
import { resyncCharts } from './resync-charts.js';
import { cleanCharts } from './clean-charts.js';
import { checkExpiredMutings } from './check-expired-mutings.js';
import { clean } from './clean.js';
const jobs = {
tickCharts,
resyncCharts,
cleanCharts,
checkExpiredMutings,
clean,
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>;
export default function(dbQueue: Bull.Queue<Record<string, unknown>>) {

View File

@ -200,7 +200,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source?.content === 'string') {
text = note.source.content;
} else if (typeof note._misskey_content === 'string') {
} else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content;
} else if (typeof note.content === 'string') {
text = htmlToMfm(note.content, note.tag);

View File

@ -82,15 +82,14 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const files = await getPromisedFiles(note.fileIds);
// text should never be undefined
const text = note.text ?? null;
const text = note.text ?? '';
let poll: Poll | null = null;
if (note.hasPoll) {
poll = await Polls.findOneBy({ noteId: note.id });
}
let apText = text ?? '';
let apText = text;
if (quote) {
apText += `\n\nRE: ${quote}`;

View File

@ -201,7 +201,7 @@ export interface IApMention extends IObject {
href: string;
}
export const isMention = (object: IObject): object is IApMention=>
export const isMention = (object: IObject): object is IApMention =>
getApType(object) === 'Mention' &&
typeof object.href === 'string';

View File

@ -1,12 +1,25 @@
import Koa from 'koa';
import { User } from '@/models/entities/user.js';
import { UserIps } from '@/models/index.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { IEndpoint } from './endpoints.js';
import authenticate, { AuthenticationError } from './authenticate.js';
import call from './call.js';
import { ApiError } from './error.js';
const userIpHistories = new Map<User['id'], Set<string>>();
setInterval(() => {
userIpHistories.clear();
}, 1000 * 60 * 60);
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
const body = ctx.request.body;
const body = ctx.is('multipart/form-data')
? (ctx.request as any).body
: ctx.method === 'GET'
? ctx.query
: ctx.request.body;
const reply = (x?: any, y?: ApiError) => {
if (x == null) {
@ -33,10 +46,38 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
authenticate(body['i']).then(([user, app]) => {
// API invoking
call(endpoint.name, user, app, body, ctx).then((res: any) => {
if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
}
reply(res);
}).catch((e: ApiError) => {
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
});
// Log IP
if (user) {
fetchMeta().then(meta => {
if (!meta.enableIpLogging) return;
const ip = ctx.ip;
const ips = userIpHistories.get(user.id);
if (ips == null || !ips.has(ip)) {
if (ips == null) {
userIpHistories.set(user.id, new Set([ip]));
} else {
ips.add(ip);
}
try {
UserIps.insert({
createdAt: new Date(),
userId: user.id,
ip: ip,
});
} catch {
}
}
});
}
}).catch(e => {
if (e instanceof AuthenticationError) {
reply(403, new ApiError({

View File

@ -1,12 +1,12 @@
import Koa from 'koa';
import { performance } from 'perf_hooks';
import { limiter } from './limiter.js';
import Koa from 'koa';
import { CacheableLocalUser, User } from '@/models/entities/user.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import { limiter } from './limiter.js';
import endpoints, { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js';
import { apiLogger } from './logger.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
const accessDenied = {
message: 'Access denied.',
@ -33,7 +33,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
throw new ApiError(accessDenied);
}
if (ep.meta.limit && !isModerator) {
if (ep.meta.limit) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
let limitActor: string;
if (user) {
@ -94,7 +94,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
}
// Cast non JSON input
if (ep.meta.requireFile && ep.params.properties) {
if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) {
for (const k of Object.keys(ep.params.properties)) {
const param = ep.params.properties![k];
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
@ -116,24 +116,24 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
// API invoking
const before = performance.now();
return await ep.exec(data, user, token, ctx?.file).catch((e: Error) => {
return await ep.exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((e: Error) => {
if (e instanceof ApiError) {
throw e;
} else {
apiLogger.error(`Internal error occurred in ${ep.name}: ${e?.message}`, {
apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, {
ep: ep.name,
ps: data,
e: {
message: e?.message,
code: e?.name,
stack: e?.stack,
message: e.message,
code: e.name,
stack: e.stack,
},
});
throw new ApiError(null, {
e: {
message: e?.message,
code: e?.name,
stack: e?.stack,
message: e.message,
code: e.name,
stack: e.stack,
},
});
}

View File

@ -1,40 +0,0 @@
import { User } from '@/models/entities/user.js';
import { id } from '@/models/id.js';
import { UserProfiles } from '@/models/index.js';
import { SelectQueryBuilder, Brackets } from 'typeorm';
function createMutesQuery(id: string) {
return UserProfiles.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: id });
}
export function generateMutedInstanceQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
const mutingQuery = createMutesQuery(me.id);
q
.andWhere(new Brackets(qb => { qb
.andWhere('note.userHost IS NULL')
.orWhere(`NOT((${ mutingQuery.getQuery() })::jsonb ? note.userHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where(`note.replyUserHost IS NULL`)
.orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.replyUserHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where(`note.renoteUserHost IS NULL`)
.orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
}));
q.setParameters(mutingQuery.getParameters());
}
export function generateMutedInstanceNotificationQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
const mutingQuery = createMutesQuery(me.id);
q.andWhere(new Brackets(qb => { qb
.andWhere('notifier.host IS NULL')
.orWhere(`NOT (( ${mutingQuery.getQuery()} )::jsonb ? notifier.host)`);
}));
q.setParameters(mutingQuery.getParameters());
}

View File

@ -1,6 +1,6 @@
import { User } from '@/models/entities/user.js';
import { Mutings } from '@/models/index.js';
import { SelectQueryBuilder, Brackets } from 'typeorm';
import { User } from '@/models/entities/user.js';
import { Mutings, UserProfiles } from '@/models/index.js';
export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }, exclude?: User) {
const mutingQuery = Mutings.createQueryBuilder('muting')
@ -11,21 +11,39 @@ export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: Use
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
}
const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id });
// 投稿の作者をミュートしていない かつ
// 投稿の返信先の作者をミュートしていない かつ
// 投稿の引用元の作者をミュートしていない
q
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
.andWhere(new Brackets(qb => { qb
.where(`note.replyUserId IS NULL`)
.where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => { qb
.where(`note.renoteUserId IS NULL`)
.where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
}))
// mute instances
.andWhere(new Brackets(qb => { qb
.andWhere('note.userHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where('note.replyUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
}));
q.setParameters(mutingQuery.getParameters());
q.setParameters(mutingInstanceQuery.getParameters());
}
export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
@ -33,8 +51,7 @@ export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: {
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });
q
.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
q.setParameters(mutingQuery.getParameters());
}

View File

@ -1,16 +1,16 @@
import * as fs from 'node:fs';
import Ajv from 'ajv';
import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js';
import { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js';
import { Schema, SchemaType } from '@/misc/schema.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js';
export type Response = Record<string, any> | void;
// TODO: paramsの型をT['params']のスキーマ定義から推論する
type executor<T extends IEndpointMeta, Ps extends Schema> =
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) =>
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
const ajv = new Ajv({
@ -20,24 +20,27 @@ const ajv = new Ajv({
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => Promise<any> {
const validate = ajv.compile(paramDef);
return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => {
function cleanup() {
fs.unlink(file.path, () => {});
}
return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => {
let cleanup: undefined | (() => void) = undefined;
if (meta.requireFile && file == null) return Promise.reject(new ApiError({
message: 'File required.',
code: 'FILE_REQUIRED',
id: '4267801e-70d1-416a-b011-4ee502885d8b',
}));
if (meta.requireFile) {
cleanup = () => {
fs.unlink(file.path, () => {});
};
if (file == null) return Promise.reject(new ApiError({
message: 'File required.',
code: 'FILE_REQUIRED',
id: '4267801e-70d1-416a-b011-4ee502885d8b',
}));
}
const valid = validate(params);
if (!valid) {
if (file) cleanup();
if (file) cleanup!();
const errors = validate.errors!;
const err = new ApiError({
@ -51,6 +54,6 @@ export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, pa
return Promise.reject(err);
}
return cb(params as SchemaType<Ps>, user, token, file, cleanup);
return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
};
}

View File

@ -35,6 +35,7 @@ import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/fed
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
import * as ep___admin_invite from './endpoints/admin/invite.js';
import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js';
import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js';
@ -59,6 +60,8 @@ import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@ -99,6 +102,7 @@ import * as ep___charts_user_notes from './endpoints/charts/user/notes.js';
import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js';
import * as ep___charts_users from './endpoints/charts/users.js';
import * as ep___clips_addNote from './endpoints/clips/add-note.js';
import * as ep___clips_removeNote from './endpoints/clips/remove-note.js';
import * as ep___clips_create from './endpoints/clips/create.js';
import * as ep___clips_delete from './endpoints/clips/delete.js';
import * as ep___clips_list from './endpoints/clips/list.js';
@ -133,6 +137,7 @@ import * as ep___federation_instances from './endpoints/federation/instances.js'
import * as ep___federation_showInstance from './endpoints/federation/show-instance.js';
import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js';
import * as ep___federation_users from './endpoints/federation/users.js';
import * as ep___federation_stats from './endpoints/federation/stats.js';
import * as ep___following_create from './endpoints/following/create.js';
import * as ep___following_delete from './endpoints/following/delete.js';
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
@ -308,6 +313,8 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js';
const eps = [
['admin/meta', ep___admin_meta],
@ -345,6 +352,7 @@ const eps = [
['admin/federation/update-instance', ep___admin_federation_updateInstance],
['admin/get-index-stats', ep___admin_getIndexStats],
['admin/get-table-stats', ep___admin_getTableStats],
['admin/get-user-ips', ep___admin_getUserIps],
['admin/invite', ep___admin_invite],
['admin/moderators/add', ep___admin_moderators_add],
['admin/moderators/remove', ep___admin_moderators_remove],
@ -369,6 +377,8 @@ const eps = [
['admin/unsuspend-user', ep___admin_unsuspendUser],
['admin/update-meta', ep___admin_updateMeta],
['admin/vacuum', ep___admin_vacuum],
['admin/delete-account', ep___admin_deleteAccount],
['admin/update-user-note', ep___admin_updateUserNote],
['announcements', ep___announcements],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
@ -409,6 +419,7 @@ const eps = [
['charts/user/reactions', ep___charts_user_reactions],
['charts/users', ep___charts_users],
['clips/add-note', ep___clips_addNote],
['clips/remove-note', ep___clips_removeNote],
['clips/create', ep___clips_create],
['clips/delete', ep___clips_delete],
['clips/list', ep___clips_list],
@ -443,6 +454,7 @@ const eps = [
['federation/show-instance', ep___federation_showInstance],
['federation/update-remote-user', ep___federation_updateRemoteUser],
['federation/users', ep___federation_users],
['federation/stats', ep___federation_stats],
['following/create', ep___following_create],
['following/delete', ep___following_delete],
['following/invalidate', ep___following_invalidate],
@ -618,6 +630,8 @@ const eps = [
['users/search', ep___users_search],
['users/show', ep___users_show],
['users/stats', ep___users_stats],
['admin/drive-capacity-override', ep___admin_driveCapOverride],
['fetch-rss', ep___fetchRss],
];
export interface IEndpointMeta {
@ -699,6 +713,16 @@ export interface IEndpointMeta {
readonly kind?: string;
readonly description?: string;
/**
* GETでのリクエストを許容するか否か
*/
readonly allowGet?: boolean;
/**
* 正常応答をキャッシュ (Cache-Control: public) する秒数
*/
readonly cacheSec?: number;
}
export interface IEndpoint {

View File

@ -0,0 +1,31 @@
import { Users } from '@/models/index.js';
import { deleteAccount } from '@/services/delete-account.js';
import define from '../../define.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireAdmin: true,
res: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => {
const user = await Users.findOneByOrFail({ id: ps.userId });
if (user.isDeleted) {
return;
}
await deleteAccount(user);
});

View File

@ -0,0 +1,47 @@
import define from '../../define.js';
import { Users } from '@/models/index.js';
import { User } from '@/models/entities/user.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
overrideMb: { type: 'number', nullable: true },
},
required: ['userId', 'overrideMb'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
if (!Users.isLocalUser(user)) {
throw new Error('user is not local user');
}
/*if (user.isAdmin) {
throw new Error('cannot suspend admin');
}
if (user.isModerator) {
throw new Error('cannot suspend moderator');
}*/
await Users.update(user.id, {
driveCapacityOverrideMb: ps.overrideMb,
});
insertModerationLog(me, 'change-drive-capacity-override', {
targetId: user.id,
});
});

View File

@ -1,5 +1,5 @@
import define from '../../../define.js';
import { DriveFiles } from '@/models/index.js';
import define from '../../../define.js';
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
export const meta = {
@ -25,8 +25,9 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
hostname: {
type: 'string',
nullable: true,
@ -41,14 +42,18 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, me) => {
const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId);
if (ps.origin === 'local') {
query.andWhere('file.userHost IS NULL');
} else if (ps.origin === 'remote') {
query.andWhere('file.userHost IS NOT NULL');
}
if (ps.userId) {
query.andWhere('file.userId = :userId', { userId: ps.userId });
} else {
if (ps.origin === 'local') {
query.andWhere('file.userHost IS NULL');
} else if (ps.origin === 'remote') {
query.andWhere('file.userHost IS NOT NULL');
}
if (ps.hostname) {
query.andWhere('file.userHost = :hostname', { hostname: ps.hostname });
if (ps.hostname) {
query.andWhere('file.userHost = :hostname', { hostname: ps.hostname });
}
}
if (ps.type) {

View File

@ -1,6 +1,6 @@
import { DriveFiles } from '@/models/index.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { DriveFiles } from '@/models/index.js';
export const meta = {
tags: ['admin'],
@ -184,5 +184,10 @@ export default define(meta, paramDef, async (ps, me) => {
throw new ApiError(meta.errors.noSuchFile);
}
if (!me.isAdmin) {
delete file.requestIp;
delete file.requestHeaders;
}
return file;
});

View File

@ -0,0 +1,31 @@
import { UserIps } from '@/models/index.js';
import define from '../../define.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireAdmin: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const ips = await UserIps.find({
where: { userId: ps.userId },
order: { createdAt: 'DESC' },
take: 30,
});
return ips.map(x => ({
ip: x.ip,
createdAt: x.createdAt.toISOString(),
}));
});

View File

@ -1,7 +1,7 @@
import config from '@/config/index.js';
import define from '../../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import define from '../../define.js';
export const meta = {
tags: ['meta'],
@ -195,6 +195,22 @@ export const meta = {
type: 'string',
optional: true, nullable: true,
},
sensitiveMediaDetection: {
type: 'string',
optional: true, nullable: false,
},
sensitiveMediaDetectionSensitivity: {
type: 'string',
optional: true, nullable: false,
},
setSensitiveFlagAutomatically: {
type: 'boolean',
optional: true, nullable: false,
},
enableSensitiveMediaDetectionForVideos: {
type: 'boolean',
optional: true, nullable: false,
},
proxyAccountId: {
type: 'string',
optional: true, nullable: true,
@ -304,6 +320,10 @@ export const meta = {
type: 'boolean',
optional: true, nullable: false,
},
enableIpLogging: {
type: 'boolean',
optional: true, nullable: false,
},
},
},
} as const;
@ -360,13 +380,16 @@ export default define(meta, paramDef, async (ps, me) => {
pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles,
useStarForReactionFallback: instance.useStarForReactionFallback,
pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey,
sensitiveMediaDetection: instance.sensitiveMediaDetection,
sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity,
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
proxyAccountId: instance.proxyAccountId,
twitterConsumerKey: instance.twitterConsumerKey,
twitterConsumerSecret: instance.twitterConsumerSecret,
@ -397,5 +420,6 @@ export default define(meta, paramDef, async (ps, me) => {
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
deeplAuthKey: instance.deeplAuthKey,
deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging,
};
});

View File

@ -99,12 +99,16 @@ export default define(meta, paramDef, async () => {
const fsStats = await si.fsSize();
const netInterface = await si.networkInterfaceDefault();
const redisServerInfo = await redisClient.info('Server');
const m = redisServerInfo.match(new RegExp('^redis_version:(.*)', 'm'));
const redis_version = m?.[1];
return {
machine: os.hostname(),
os: os.platform(),
node: process.version,
psql: await db.query('SHOW server_version').then(x => x[0].server_version),
redis: redisClient.server_info.redis_version,
redis: redis_version,
cpu: {
model: os.cpus()[0].model,
cores: os.cpus().length,

View File

@ -25,7 +25,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, me) => {
const [user, profile] = await Promise.all([
Users.findOneBy({ id: ps.userId }),
UserProfiles.findOneBy({ userId: ps.userId })
UserProfiles.findOneBy({ userId: ps.userId }),
]);
if (user == null || profile == null) {
@ -58,6 +58,7 @@ export default define(meta, paramDef, async (ps, me) => {
autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle,
alwaysMarkNsfw: profile.alwaysMarkNsfw,
autoSensitive: profile.autoSensitive,
carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
@ -68,6 +69,8 @@ export default define(meta, paramDef, async (ps, me) => {
isModerator: user.isModerator,
isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
lastActiveDate: user.lastActiveDate,
moderationNote: profile.moderationNote,
signins,
};
});

View File

@ -25,7 +25,7 @@ export const paramDef = {
offset: { type: 'integer', default: 0 },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
username: { type: 'string', nullable: true, default: null },
hostname: {
type: 'string',
@ -61,7 +61,7 @@ export default define(meta, paramDef, async (ps, me) => {
}
if (ps.hostname) {
query.andWhere('user.host like :hostname', { hostname: '%' + ps.hostname.toLowerCase() + '%' });
query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() });
}
switch (ps.sort) {

View File

@ -1,8 +1,8 @@
import define from '../../define.js';
import { Meta } from '@/models/entities/meta.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js';
import { db } from '@/db/postgre.js';
import define from '../../define.js';
export const meta = {
tags: ['admin'],
@ -48,6 +48,10 @@ export const paramDef = {
enableRecaptcha: { type: 'boolean' },
recaptchaSiteKey: { type: 'string', nullable: true },
recaptchaSecretKey: { type: 'string', nullable: true },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' },
enableSensitiveMediaDetectionForVideos: { type: 'boolean' },
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
maintainerName: { type: 'string', nullable: true },
maintainerEmail: { type: 'string', nullable: true },
@ -96,6 +100,7 @@ export const paramDef = {
objectStorageUseProxy: { type: 'boolean' },
objectStorageSetPublicRead: { type: 'boolean' },
objectStorageS3ForcePathStyle: { type: 'boolean' },
enableIpLogging: { type: 'boolean' },
},
required: [],
} as const;
@ -212,6 +217,22 @@ export default define(meta, paramDef, async (ps, me) => {
set.recaptchaSecretKey = ps.recaptchaSecretKey;
}
if (ps.sensitiveMediaDetection !== undefined) {
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
}
if (ps.sensitiveMediaDetectionSensitivity !== undefined) {
set.sensitiveMediaDetectionSensitivity = ps.sensitiveMediaDetectionSensitivity;
}
if (ps.setSensitiveFlagAutomatically !== undefined) {
set.setSensitiveFlagAutomatically = ps.setSensitiveFlagAutomatically;
}
if (ps.enableSensitiveMediaDetectionForVideos !== undefined) {
set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos;
}
if (ps.proxyAccountId !== undefined) {
set.proxyAccountId = ps.proxyAccountId;
}
@ -396,6 +417,10 @@ export default define(meta, paramDef, async (ps, me) => {
set.deeplIsPro = ps.deeplIsPro;
}
if (ps.enableIpLogging !== undefined) {
set.enableIpLogging = ps.enableIpLogging;
}
await db.transaction(async transactionalEntityManager => {
const metas = await transactionalEntityManager.find(Meta, {
order: {

View File

@ -0,0 +1,31 @@
import { UserProfiles, Users } from '@/models/index.js';
import define from '../../define.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
text: { type: 'string' },
},
required: ['userId', 'text'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
await UserProfiles.update({ userId: user.id }, {
moderationNote: ps.text,
});
});

View File

@ -1,5 +1,5 @@
import define from '../define.js';
import { Announcements, AnnouncementReads } from '@/models/index.js';
import define from '../define.js';
import { makePaginationQuery } from '../common/make-pagination-query.js';
export const meta = {

View File

@ -2,12 +2,13 @@ import define from '../../define.js';
import config from '@/config/index.js';
import { createPerson } from '@/remote/activitypub/models/person.js';
import { createNote } from '@/remote/activitypub/models/note.js';
import DbResolver from '@/remote/activitypub/db-resolver.js';
import Resolver from '@/remote/activitypub/resolver.js';
import { ApiError } from '../../error.js';
import { extractDbHost } from '@/misc/convert-host.js';
import { Users, Notes } from '@/models/index.js';
import { Note } from '@/models/entities/note.js';
import { User } from '@/models/entities/user.js';
import { CacheableLocalUser, User } from '@/models/entities/user.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { isActor, isPost, getApId } from '@/remote/activitypub/type.js';
import ms from 'ms';
@ -77,8 +78,8 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => {
const object = await fetchAny(ps.uri);
export default define(meta, paramDef, async (ps, me) => {
const object = await fetchAny(ps.uri, me);
if (object) {
return object;
} else {
@ -89,48 +90,18 @@ export default define(meta, paramDef, async (ps) => {
/***
* URIからUserかNoteを解決する
*/
async function fetchAny(uri: string): Promise<SchemaType<typeof meta['res']> | null> {
// URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ
if (uri.startsWith(config.url + '/')) {
const parts = uri.split('/');
const id = parts.pop();
const type = parts.pop();
if (type === 'notes') {
const note = await Notes.findOneBy({ id });
if (note) {
return {
type: 'Note',
object: await Notes.pack(note, null, { detail: true }),
};
}
} else if (type === 'users') {
const user = await Users.findOneBy({ id });
if (user) {
return {
type: 'User',
object: await Users.pack(user, null, { detail: true }),
};
}
}
}
async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// ブロックしてたら中断
const fetchedMeta = await fetchMeta();
if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null;
// URI(AP Object id)としてDB検索
{
const [user, note] = await Promise.all([
Users.findOneBy({ uri: uri }),
Notes.findOneBy({ uri: uri }),
]);
const dbResolver = new DbResolver();
const packed = await mergePack(user, note);
if (packed !== null) return packed;
}
let local = await mergePack(me, ...await Promise.all([
dbResolver.getUserFromApId(uri),
dbResolver.getNoteFromApId(uri),
]));
if (local != null) return local;
// リモートから一旦オブジェクトフェッチ
const resolver = new Resolver();
@ -139,74 +110,37 @@ async function fetchAny(uri: string): Promise<SchemaType<typeof meta['res']> | n
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
// これはDBに存在する可能性があるため再度DB検索
if (uri !== object.id) {
if (object.id.startsWith(config.url + '/')) {
const parts = object.id.split('/');
const id = parts.pop();
const type = parts.pop();
if (type === 'notes') {
const note = await Notes.findOneBy({ id });
if (note) {
return {
type: 'Note',
object: await Notes.pack(note, null, { detail: true }),
};
}
} else if (type === 'users') {
const user = await Users.findOneBy({ id });
if (user) {
return {
type: 'User',
object: await Users.pack(user, null, { detail: true }),
};
}
}
}
const [user, note] = await Promise.all([
Users.findOneBy({ uri: object.id }),
Notes.findOneBy({ uri: object.id }),
]);
const packed = await mergePack(user, note);
if (packed !== null) return packed;
local = await mergePack(me, ...await Promise.all([
dbResolver.getUserFromApId(object.id),
dbResolver.getNoteFromApId(object.id),
]));
if (local != null) return local;
}
// それでもみつからなければ新規であるため登録
if (isActor(object)) {
const user = await createPerson(getApId(object));
return {
type: 'User',
object: await Users.pack(user, null, { detail: true }),
};
}
if (isPost(object)) {
const note = await createNote(getApId(object), undefined, true);
return {
type: 'Note',
object: await Notes.pack(note!, null, { detail: true }),
};
}
return null;
return await mergePack(
me,
isActor(object) ? await createPerson(getApId(object)) : null,
isPost(object) ? await createNote(getApId(object), undefined, true) : null,
);
}
async function mergePack(user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
async function mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
if (user != null) {
return {
type: 'User',
object: await Users.pack(user, null, { detail: true }),
object: await Users.pack(user, me, { detail: true }),
};
}
} else if (note != null) {
try {
const object = await Notes.pack(note, me, { detail: true });
if (note != null) {
return {
type: 'Note',
object: await Notes.pack(note, null, { detail: true }),
};
return {
type: 'Note',
object,
};
} catch (e) {
return null;
}
}
return null;

View File

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'users'],
res: getJsonSchema(activeUsersChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { apRequestChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts'],
res: getJsonSchema(apRequestChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { driveChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'drive'],
res: getJsonSchema(driveChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { federationChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts'],
res: getJsonSchema(federationChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { hashtagChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'hashtags'],
res: getJsonSchema(hashtagChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { instanceChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts'],
res: getJsonSchema(instanceChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { notesChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'notes'],
res: getJsonSchema(notesChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { perUserDriveChart } from '@/services/chart/index.js';
import define from '../../../define.js';
export const meta = {
tags: ['charts', 'drive', 'users'],
res: getJsonSchema(perUserDriveChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -6,6 +6,9 @@ export const meta = {
tags: ['charts', 'users', 'following'],
res: getJsonSchema(perUserFollowingChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { perUserNotesChart } from '@/services/chart/index.js';
import define from '../../../define.js';
export const meta = {
tags: ['charts', 'users', 'notes'],
res: getJsonSchema(perUserNotesChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { perUserReactionsChart } from '@/services/chart/index.js';
import define from '../../../define.js';
export const meta = {
tags: ['charts', 'users', 'reactions'],
res: getJsonSchema(perUserReactionsChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { usersChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'users'],
res: getJsonSchema(usersChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View File

@ -0,0 +1,57 @@
import define from '../../define.js';
import { ClipNotes, Clips } from '@/models/index.js';
import { ApiError } from '../../error.js';
import { getNote } from '../../common/getters.js';
export const meta = {
tags: ['account', 'notes', 'clips'],
requireCredential: true,
kind: 'write:account',
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52',
},
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'aff017de-190e-434b-893e-33a9ff5049d8',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
clipId: { type: 'string', format: 'misskey:id' },
noteId: { type: 'string', format: 'misskey:id' },
},
required: ['clipId', 'noteId'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const clip = await Clips.findOneBy({
id: ps.clipId,
userId: user.id,
});
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw e;
});
await ClipNotes.delete({
noteId: note.id,
clipId: clip.id,
});
});

View File

@ -1,6 +1,6 @@
import define from '../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { DriveFiles } from '@/models/index.js';
import define from '../define.js';
export const meta = {
tags: ['drive', 'account'],
@ -39,7 +39,7 @@ export default define(meta, paramDef, async (ps, user) => {
const usage = await DriveFiles.calcDriveUsageOf(user.id);
return {
capacity: 1024 * 1024 * instance.localDriveCapacityMb,
capacity: 1024 * 1024 * (user.driveCapacityOverrideMb || instance.localDriveCapacityMb),
usage: usage,
};
});

View File

@ -1,10 +1,12 @@
import ms from 'ms';
import { addFile } from '@/services/drive/add-file.js';
import { DriveFiles } from '@/models/index.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import define from '../../../define.js';
import { apiLogger } from '../../../logger.js';
import { ApiError } from '../../../error.js';
import { DriveFiles } from '@/models/index.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
export const meta = {
tags: ['drive'],
@ -34,6 +36,18 @@ export const meta = {
code: 'INVALID_FILE_NAME',
id: 'f449b209-0c60-4e51-84d5-29486263bfd4',
},
inappropriate: {
message: 'Cannot upload the file because it has been determined that it possibly contains inappropriate content.',
code: 'INAPPROPRIATE',
id: 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2',
},
noFreeSpace: {
message: 'Cannot upload the file because you have no free space of drive.',
code: 'NO_FREE_SPACE',
id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064',
},
},
} as const;
@ -50,7 +64,7 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
export default define(meta, paramDef, async (ps, user, _, file, cleanup, ip, headers) => {
// Get 'name' parameter
let name = ps.name || file.originalname;
if (name !== undefined && name !== null) {
@ -66,14 +80,30 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
name = null;
}
const meta = await fetchMeta();
try {
// Create file
const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive });
const driveFile = await addFile({
user,
path: file.path,
name,
comment: ps.comment,
folderId: ps.folderId,
force: ps.force,
sensitive: ps.isSensitive,
requestIp: meta.enableIpLogging ? ip : null,
requestHeaders: meta.enableIpLogging ? headers : null,
});
return await DriveFiles.pack(driveFile, { self: true });
} catch (e) {
if (e instanceof Error || typeof e === 'string') {
apiLogger.error(e);
}
if (e instanceof IdentifiableError) {
if (e.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate);
if (e.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace);
}
throw new ApiError();
} finally {
cleanup!();

View File

@ -1,8 +1,8 @@
import { publishDriveStream } from '@/services/stream.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { DriveFiles, DriveFolders, Users } from '@/models/index.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['drive'],

View File

@ -1,9 +1,9 @@
import ms from 'ms';
import { uploadFromUrl } from '@/services/drive/upload-from-url.js';
import define from '../../../define.js';
import { DriveFiles } from '@/models/index.js';
import { publishMainStream } from '@/services/stream.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
import define from '../../../define.js';
export const meta = {
tags: ['drive'],
@ -34,8 +34,8 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment }).then(file => {
export default define(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => {
uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => {
DriveFiles.pack(file, { self: true }).then(packedFile => {
publishMainStream(user.id, 'urlUploadFinished', {
marker: ps.marker,

View File

@ -1,6 +1,6 @@
import define from '../define.js';
import { createExportCustomEmojisJob } from '@/queue/index.js';
import ms from 'ms';
import { createExportCustomEmojisJob } from '@/queue/index.js';
import define from '../define.js';
export const meta = {
secure: true,

View File

@ -0,0 +1,65 @@
import { IsNull, MoreThan, Not } from 'typeorm';
import { Followings, Instances } from '@/models/index.js';
import { awaitAll } from '@/prelude/await-all.js';
import define from '../../define.js';
export const meta = {
tags: ['federation'],
requireCredential: false,
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => {
const [topSubInstances, topPubInstances, allSubCount, allPubCount] = await Promise.all([
Instances.find({
where: {
followersCount: MoreThan(0),
},
order: {
followersCount: 'DESC',
},
take: ps.limit,
}),
Instances.find({
where: {
followingCount: MoreThan(0),
},
order: {
followingCount: 'DESC',
},
take: ps.limit,
}),
Followings.count({
where: {
followeeHost: Not(IsNull()),
},
}),
Followings.count({
where: {
followerHost: Not(IsNull()),
},
}),
]);
const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0);
const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
return await awaitAll({
topSubInstances: Instances.packMany(topSubInstances),
otherFollowersCount: Math.max(0, allSubCount - gotSubCount),
topPubInstances: Instances.packMany(topPubInstances),
otherFollowingCount: Math.max(0, allPubCount - gotPubCount),
});
});

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