Merge tag '12.112.1' into develop
@ -5,6 +5,6 @@
|
||||
"loader=./test/loader.js"
|
||||
],
|
||||
"slow": 1000,
|
||||
"timeout": 10000,
|
||||
"timeout": 30000,
|
||||
"exit": true
|
||||
}
|
||||
|
5
packages/backend/assets/notification-badges/LICENSE
Normal file
@ -0,0 +1,5 @@
|
||||
Font Awesome Icons
|
||||
-------------------------
|
||||
|
||||
Ⓒ Font Awesome
|
||||
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
BIN
packages/backend/assets/notification-badges/at.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/backend/assets/notification-badges/check.png
Normal file
After Width: | Height: | Size: 577 B |
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/backend/assets/notification-badges/clock.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/backend/assets/notification-badges/comments.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/backend/assets/notification-badges/id-card-alt.png
Normal file
After Width: | Height: | Size: 844 B |
BIN
packages/backend/assets/notification-badges/null.png
Normal file
After Width: | Height: | Size: 174 B |
BIN
packages/backend/assets/notification-badges/plus.png
Normal file
After Width: | Height: | Size: 507 B |
BIN
packages/backend/assets/notification-badges/poll-h.png
Normal file
After Width: | Height: | Size: 689 B |
BIN
packages/backend/assets/notification-badges/quote-right.png
Normal file
After Width: | Height: | Size: 772 B |
BIN
packages/backend/assets/notification-badges/reply.png
Normal file
After Width: | Height: | Size: 930 B |
BIN
packages/backend/assets/notification-badges/retweet.png
Normal file
After Width: | Height: | Size: 798 B |
BIN
packages/backend/assets/notification-badges/user-plus.png
Normal file
After Width: | Height: | Size: 991 B |
23
packages/backend/migration/1655368940105-nsfw-detection.js
Normal 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"`);
|
||||
}
|
||||
}
|
15
packages/backend/migration/1655371960534-nsfw-detection-2.js
Normal 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"`);
|
||||
}
|
||||
}
|
21
packages/backend/migration/1655388169582-nsfw-detection-3.js
Normal 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"`);
|
||||
}
|
||||
}
|
25
packages/backend/migration/1655393015659-nsfw-detection-4.js
Normal 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'`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
17
packages/backend/migration/1655918165614-user-ip.js
Normal 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"`);
|
||||
}
|
||||
}
|
13
packages/backend/migration/1656122560740-file-ip.js
Normal 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"`);
|
||||
}
|
||||
}
|
33
packages/backend/migration/1656251734807-nsfw-detection-5.js
Normal 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") `);
|
||||
}
|
||||
}
|
13
packages/backend/migration/1656328812281-ip-2.js
Normal 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`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1656408772602-nsfw-detection-6.js
Normal 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"`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
BIN
packages/backend/nsfw-model/group1-shard1of6
Normal file
2
packages/backend/nsfw-model/group1-shard2of6
Normal file
3
packages/backend/nsfw-model/group1-shard3of6
Normal file
3
packages/backend/nsfw-model/group1-shard4of6
Normal file
18
packages/backend/nsfw-model/group1-shard5of6
Normal file
3
packages/backend/nsfw-model/group1-shard6of6
Normal file
1
packages/backend/nsfw-model/model.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ export type Source = {
|
||||
redis: {
|
||||
host: string;
|
||||
port: number;
|
||||
family?: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
|
@ -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')
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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]);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
8
packages/backend/src/misc/is-mime-image.ts
Normal 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);
|
@ -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;
|
||||
}
|
15
packages/backend/src/misc/is-user-related.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
24
packages/backend/src/models/entities/user-ip.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
})
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 * * * *' },
|
||||
|
@ -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,
|
||||
},
|
||||
|
18
packages/backend/src/queue/processors/system/clean.ts
Normal 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();
|
||||
}
|
@ -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>>) {
|
||||
|
@ -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);
|
||||
|
@ -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}`;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
});
|
@ -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,
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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(),
|
||||
}));
|
||||
});
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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: {
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
@ -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!();
|
||||
|
@ -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'],
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
});
|
||||
});
|