Merge branch 'develop' into mahjong
This commit is contained in:
@@ -19,5 +19,6 @@
|
||||
},
|
||||
"target": "es2022"
|
||||
},
|
||||
"minify": false
|
||||
"minify": false,
|
||||
"sourceMaps": "inline"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
||||
<script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { loadConfig } from './built/config.js'
|
||||
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const config = loadConfig();
|
||||
const spec = genOpenapiSpec(config, true);
|
||||
|
||||
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class UserBlacklistAnntena1689325027964 {
|
||||
name = 'UserBlacklistAnntena1689325027964'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class FixRenoteMuting1690417561185 {
|
||||
name = 'FixRenoteMuting1690417561185'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ChangeCacheRemoteFilesDefault1690417561186 {
|
||||
name = 'ChangeCacheRemoteFilesDefault1690417561186'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Fix1690417561187 {
|
||||
name = 'Fix1690417561187'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class User2faBackupCodes1690569881926 {
|
||||
name = 'User2faBackupCodes1690569881926'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RefineAnnouncement1691649257651 {
|
||||
name = 'RefineAnnouncement1691649257651'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RefineAnnouncement21691657412740 {
|
||||
name = 'RefineAnnouncement21691657412740'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class VerifiedLinks1695260774117 {
|
||||
name = 'VerifiedLinks1695260774117'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class FollowingNotify1695288787870 {
|
||||
name = 'FollowingNotify1695288787870'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ShortName1695440131671 {
|
||||
name = 'ShortName1695440131671'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class MutingNotificationTypes1695605508898 {
|
||||
name = 'MutingNotificationTypes1695605508898'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class NoteUpdatedAt1695901659683 {
|
||||
name = 'NoteUpdatedAt1695901659683'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class UserListMembership1696323464251 {
|
||||
name = 'UserListMembership1696323464251'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Hibernation1696331570827 {
|
||||
name = 'Hibernation1696331570827'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Clean1696332072038 {
|
||||
name = 'Clean1696332072038'
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class HardMute1700383825690 {
|
||||
name = 'HardMute1700383825690'
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class MakeRepositoryUrlNullable1707808106310 {
|
||||
name = 'MakeRepositoryUrlNullable1707808106310'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" DROP NOT NULL`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET NOT NULL`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RepositoryUrlFromSyuiloToMisskeyDev1708266695091 {
|
||||
name = 'RepositoryUrlFromSyuiloToMisskeyDev1708266695091'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`UPDATE "meta" SET "repositoryUrl" = 'https://github.com/misskey-dev/misskey' WHERE "repositoryUrl" = 'https://github.com/syuilo/misskey'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
// no valid down migration
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class PerInstanceModNote1708399372194 {
|
||||
name = 'PerInstanceModNote1708399372194'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "instance" ADD "moderationNote" character varying(16384) NOT NULL DEFAULT ''`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "moderationNote"`);
|
||||
}
|
||||
}
|
||||
42
packages/backend/migration/1710512074000-url-preview-meta.js
Normal file
42
packages/backend/migration/1710512074000-url-preview-meta.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class UrlPreviewMeta1710512074000 {
|
||||
name = 'UrlPreviewMeta1710512074000'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
alter table meta
|
||||
rename column "summalyProxy" to "urlPreviewSummaryProxyUrl";
|
||||
alter table meta
|
||||
add "urlPreviewEnabled" boolean default true not null;
|
||||
alter table meta
|
||||
add "urlPreviewTimeout" integer default 10000 not null;
|
||||
alter table meta
|
||||
add "urlPreviewMaximumContentLength" bigint default 10485760 not null;
|
||||
alter table meta
|
||||
add "urlPreviewRequireContentLength" boolean default false not null;
|
||||
alter table meta
|
||||
add "urlPreviewUserAgent" varchar(1024) default null;
|
||||
`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
alter table meta
|
||||
rename column "urlPreviewSummaryProxyUrl" to "summalyProxy";
|
||||
alter table meta
|
||||
drop column "urlPreviewEnabled";
|
||||
alter table meta
|
||||
drop column "urlPreviewTimeout";
|
||||
alter table meta
|
||||
drop column "urlPreviewMaximumContentLength";
|
||||
alter table meta
|
||||
drop column "urlPreviewRequireContentLength";
|
||||
alter table meta
|
||||
drop column "urlPreviewUserAgent";
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AntennaExcludeBots1710919614510 {
|
||||
name = 'AntennaExcludeBots1710919614510'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeBots" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeBots"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AbuseReportNotification1713656541000 {
|
||||
name = 'AbuseReportNotification1713656541000'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "system_webhook" (
|
||||
"id" varchar(32) NOT NULL,
|
||||
"isActive" boolean NOT NULL DEFAULT true,
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"latestSentAt" timestamp with time zone NULL DEFAULT NULL,
|
||||
"latestStatus" integer NULL DEFAULT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[],
|
||||
"url" varchar(1024) NOT NULL,
|
||||
"secret" varchar(1024) NOT NULL,
|
||||
CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive");
|
||||
CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on");
|
||||
|
||||
CREATE TABLE "abuse_report_notification_recipient" (
|
||||
"id" varchar(32) NOT NULL,
|
||||
"isActive" boolean NOT NULL DEFAULT true,
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"method" varchar(64) NOT NULL,
|
||||
"userId" varchar(32) NULL DEFAULT NULL,
|
||||
"systemWebhookId" varchar(32) NULL DEFAULT NULL,
|
||||
CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION,
|
||||
CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
);
|
||||
CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive");
|
||||
CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method");
|
||||
CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId");
|
||||
CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId");
|
||||
`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1";
|
||||
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2";
|
||||
ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId";
|
||||
DROP INDEX "IDX_abuse_report_notification_recipient_isActive";
|
||||
DROP INDEX "IDX_abuse_report_notification_recipient_method";
|
||||
DROP INDEX "IDX_abuse_report_notification_recipient_userId";
|
||||
DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId";
|
||||
DROP TABLE "abuse_report_notification_recipient";
|
||||
|
||||
DROP INDEX "IDX_system_webhook_isActive";
|
||||
DROP INDEX "IDX_system_webhook_on";
|
||||
DROP TABLE "system_webhook";
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ChannelIdDenormalizedForMiPoll1716129964060 {
|
||||
name = 'ChannelIdDenormalizedForMiPoll1716129964060'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "poll" ADD "channelId" character varying(32)`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_c1240fcc9675946ea5d6c2860e" ON "poll" ("channelId") `);
|
||||
await queryRunner.query(`UPDATE "poll" SET "channelId" = "note"."channelId" FROM "note" WHERE "poll"."noteId" = "note"."id"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_c1240fcc9675946ea5d6c2860e"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`ALTER TABLE "poll" DROP COLUMN "channelId"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class NotRespondingSince1716345015347 {
|
||||
name = 'NotRespondingSince1716345015347'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "instance" ADD "notRespondingSince" TIMESTAMP WITH TIME ZONE`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "notRespondingSince"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class SuspensionStateInsteadOfIsSspended1716345771510 {
|
||||
name = 'SuspensionStateInsteadOfIsSspended1716345771510'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."instance_suspensionstate_enum" AS ENUM('none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding')`);
|
||||
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_34500da2e38ac393f7bb6b299c"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "isSuspended" TO "suspensionState"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE "public"."instance_suspensionstate_enum" USING (
|
||||
CASE "suspensionState"
|
||||
WHEN TRUE THEN 'manuallySuspended'::instance_suspensionstate_enum
|
||||
ELSE 'none'::instance_suspensionstate_enum
|
||||
END
|
||||
)`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT 'none'`);
|
||||
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3ede46f507c87ad698051d56a8" ON "instance" ("suspensionState") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3ede46f507c87ad698051d56a8"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE boolean USING (
|
||||
CASE "suspensionState"
|
||||
WHEN 'none'::instance_suspensionstate_enum THEN FALSE
|
||||
ELSE TRUE
|
||||
END
|
||||
)`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT false`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "suspensionState" TO "isSuspended"`);
|
||||
|
||||
await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `);
|
||||
|
||||
await queryRunner.query(`DROP TYPE "public"."instance_suspensionstate_enum"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RemoveAntennaNotify1716450883149 {
|
||||
name = 'RemoveAntennaNotify1716450883149'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "notify"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ADD "notify" boolean NOT NULL`);
|
||||
}
|
||||
}
|
||||
16
packages/backend/migration/1717117195275-inquiryUrl.js
Normal file
16
packages/backend/migration/1717117195275-inquiryUrl.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class InquiryUrl1717117195275 {
|
||||
name = 'InquiryUrl1717117195275'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "inquiryUrl" character varying(1024)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "inquiryUrl"`);
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,22 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.10.0"
|
||||
"node": "^20.10.0 || ^22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/boot/entry.js",
|
||||
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"check:connect": "node ./check_connect.js",
|
||||
"build": "swc src -d built -D",
|
||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc",
|
||||
"watch:swc": "swc src -d built -D -w",
|
||||
"check:connect": "node ./scripts/check_connect.js",
|
||||
"build": "swc src -d built -D --strip-leading-paths",
|
||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
|
||||
"watch:swc": "swc src -d built -D -w --strip-leading-paths",
|
||||
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||
"watch": "node watch.mjs",
|
||||
"watch": "node ./scripts/watch.mjs",
|
||||
"restart": "pnpm build && pnpm start",
|
||||
"dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "node ./scripts/dev.mjs",
|
||||
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
|
||||
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
|
||||
@@ -31,7 +31,7 @@
|
||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"generate-api-json": "pnpm build && node ./generate_api_json.js"
|
||||
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
@@ -67,38 +67,41 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.412.0",
|
||||
"@aws-sdk/lib-storage": "3.412.0",
|
||||
"@bull-board/api": "5.14.0",
|
||||
"@bull-board/fastify": "5.14.0",
|
||||
"@bull-board/ui": "5.14.0",
|
||||
"@discordapp/twemoji": "15.0.2",
|
||||
"@bull-board/api": "5.17.0",
|
||||
"@bull-board/fastify": "5.17.0",
|
||||
"@bull-board/ui": "5.17.0",
|
||||
"@discordapp/twemoji": "15.0.3",
|
||||
"@fastify/accepts": "4.3.0",
|
||||
"@fastify/cookie": "9.3.1",
|
||||
"@fastify/cors": "8.5.0",
|
||||
"@fastify/express": "2.3.0",
|
||||
"@fastify/http-proxy": "9.3.0",
|
||||
"@fastify/multipart": "8.1.0",
|
||||
"@fastify/static": "6.12.0",
|
||||
"@fastify/view": "8.2.0",
|
||||
"@misskey-dev/sharp-read-bmp": "^1.1.1",
|
||||
"@misskey-dev/summaly": "^5.0.3",
|
||||
"@nestjs/common": "10.2.10",
|
||||
"@nestjs/core": "10.2.10",
|
||||
"@nestjs/testing": "10.2.10",
|
||||
"@fastify/cors": "9.0.1",
|
||||
"@fastify/express": "3.0.0",
|
||||
"@fastify/http-proxy": "9.5.0",
|
||||
"@fastify/multipart": "8.2.0",
|
||||
"@fastify/static": "7.0.3",
|
||||
"@fastify/view": "9.1.0",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.1.0",
|
||||
"@napi-rs/canvas": "^0.1.52",
|
||||
"@nestjs/common": "10.3.8",
|
||||
"@nestjs/core": "10.3.8",
|
||||
"@nestjs/testing": "10.3.8",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@simplewebauthn/server": "9.0.2",
|
||||
"@sentry/node": "^8.5.0",
|
||||
"@sentry/profiling-node": "^8.5.0",
|
||||
"@simplewebauthn/server": "10.0.0",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@smithy/node-http-handler": "2.1.10",
|
||||
"@swc/cli": "0.1.63",
|
||||
"@swc/core": "1.3.107",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
"@smithy/node-http-handler": "2.5.0",
|
||||
"@swc/cli": "0.3.12",
|
||||
"@swc/core": "1.4.17",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "6.0.1",
|
||||
"async-mutex": "0.4.1",
|
||||
"ajv": "8.13.0",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.2",
|
||||
"bullmq": "5.1.9",
|
||||
"bullmq": "5.7.8",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"chalk": "5.3.0",
|
||||
@@ -109,26 +112,27 @@
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "4.25.2",
|
||||
"fastify": "4.26.2",
|
||||
"fastify-raw-body": "4.3.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.0.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.1.0",
|
||||
"got": "14.2.1",
|
||||
"happy-dom": "10.0.3",
|
||||
"hpagent": "1.2.0",
|
||||
"http-link-header": "1.1.1",
|
||||
"ioredis": "5.3.2",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
"ioredis": "5.4.1",
|
||||
"ip-cidr": "3.1.0",
|
||||
"ipaddr.js": "2.1.0",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "5.0.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "23.2.0",
|
||||
"jsdom": "24.0.0",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.2",
|
||||
"jsrsasign": "11.0.0",
|
||||
"meilisearch": "0.37.0",
|
||||
"jsrsasign": "11.1.0",
|
||||
"meilisearch": "0.38.0",
|
||||
"mfm-js": "0.24.0",
|
||||
"microformats-parser": "2.0.2",
|
||||
"mime-types": "2.1.35",
|
||||
@@ -136,90 +140,87 @@
|
||||
"misskey-reversi": "workspace:*",
|
||||
"misskey-mahjong": "workspace:*",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"nanoid": "5.0.4",
|
||||
"nanoid": "5.0.7",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.8",
|
||||
"nodemailer": "6.9.13",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "0.10.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.2.2",
|
||||
"otpauth": "9.2.3",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.11.3",
|
||||
"pg": "8.11.5",
|
||||
"pkce-challenge": "4.1.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.3.1",
|
||||
"pureimage": "0.3.17",
|
||||
"qrcode": "1.5.3",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.20.9",
|
||||
"re2": "1.21.2",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.1.14",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.1",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sanitize-html": "2.13.0",
|
||||
"secure-json-parse": "2.7.0",
|
||||
"sharp": "0.32.6",
|
||||
"sharp": "0.33.3",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.21.24",
|
||||
"systeminformation": "5.22.7",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.8",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.20",
|
||||
"typescript": "5.3.3",
|
||||
"typescript": "5.5.2",
|
||||
"ulid": "2.3.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.16.0",
|
||||
"ws": "8.17.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@nestjs/platform-express": "10.3.1",
|
||||
"@simplewebauthn/typescript-types": "8.3.4",
|
||||
"@swc/jest": "0.2.31",
|
||||
"@nestjs/platform-express": "10.3.8",
|
||||
"@simplewebauthn/types": "10.0.0",
|
||||
"@swc/jest": "0.2.36",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "6.0.2",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
"@types/body-parser": "1.19.5",
|
||||
"@types/cbor": "6.0.0",
|
||||
"@types/color-convert": "2.0.3",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/fluent-ffmpeg": "2.1.24",
|
||||
"@types/htmlescape": "^1.1.3",
|
||||
"@types/http-link-header": "1.0.5",
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsdom": "21.1.6",
|
||||
"@types/jsonld": "1.5.13",
|
||||
"@types/jsrsasign": "10.5.12",
|
||||
"@types/jsrsasign": "10.5.14",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.11.17",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/node": "20.12.7",
|
||||
"@types/nodemailer": "6.4.15",
|
||||
"@types/oauth": "0.9.4",
|
||||
"@types/oauth2orize": "1.11.3",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.11.0",
|
||||
"@types/pg": "8.11.5",
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/punycode": "2.1.3",
|
||||
"@types/punycode": "2.1.4",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/random-seed": "0.3.5",
|
||||
"@types/ratelimiter": "3.4.6",
|
||||
"@types/rename": "1.0.7",
|
||||
"@types/sanitize-html": "2.9.5",
|
||||
"@types/semver": "7.5.6",
|
||||
"@types/sharp": "0.32.0",
|
||||
"@types/sanitize-html": "2.11.0",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
@@ -227,17 +228,17 @@
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.3",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
||||
"@typescript-eslint/parser": "7.7.1",
|
||||
"aws-sdk-client-mock": "3.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.56.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"execa": "8.0.1",
|
||||
"fkill": "^9.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.0.3",
|
||||
"nodemon": "3.1.0",
|
||||
"pid-port": "1.0.0",
|
||||
"simple-oauth2": "5.0.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import Redis from 'ioredis';
|
||||
import { loadConfig } from './built/config.js';
|
||||
import { loadConfig } from '../built/config.js';
|
||||
|
||||
const config = loadConfig();
|
||||
const redis = new Redis(config.redis);
|
||||
61
packages/backend/scripts/dev.mjs
Normal file
61
packages/backend/scripts/dev.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { execa, execaNode } from 'execa';
|
||||
|
||||
/** @type {import('execa').ExecaChildProcess | undefined} */
|
||||
let backendProcess;
|
||||
|
||||
async function execBuildAssets() {
|
||||
await execa('pnpm', ['run', 'build-assets'], {
|
||||
cwd: '../../',
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
})
|
||||
}
|
||||
|
||||
function execStart() {
|
||||
// pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので
|
||||
// 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい
|
||||
backendProcess = execaNode('./built/boot/entry.js', [], {
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
env: {
|
||||
'NODE_ENV': 'development',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function killProc() {
|
||||
if (backendProcess) {
|
||||
backendProcess.kill();
|
||||
await new Promise(resolve => backendProcess.on('exit', resolve));
|
||||
backendProcess = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
execaNode(
|
||||
'./node_modules/nodemon/bin/nodemon.js',
|
||||
[
|
||||
'-w', 'src',
|
||||
'-e', 'ts,js,mjs,cjs,json',
|
||||
'--exec', 'pnpm', 'run', 'build',
|
||||
],
|
||||
{
|
||||
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
|
||||
})
|
||||
.on('message', async (message) => {
|
||||
if (message.type === 'exit') {
|
||||
// かならずbuild->build-assetsの順番で呼び出したいので、
|
||||
// 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。
|
||||
// pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある
|
||||
|
||||
await killProc();
|
||||
await execBuildAssets();
|
||||
execStart();
|
||||
}
|
||||
})
|
||||
})();
|
||||
13
packages/backend/scripts/generate_api_json.js
Normal file
13
packages/backend/scripts/generate_api_json.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { loadConfig } from '../built/config.js'
|
||||
import { genOpenapiSpec } from '../built/server/api/openapi/gen-spec.js'
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const config = loadConfig();
|
||||
const spec = genOpenapiSpec(config, true);
|
||||
|
||||
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
||||
@@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
|
||||
import Logger from '@/logger.js';
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const nestLogger = logger.createSubLogger('nest', 'green', false);
|
||||
const nestLogger = logger.createSubLogger('nest', 'green');
|
||||
|
||||
export class NestLogger implements LoggerService {
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ import Logger from '@/logger.js';
|
||||
import { envOption } from '../env.js';
|
||||
import { masterMain } from './master.js';
|
||||
import { workerMain } from './worker.js';
|
||||
import { readyRef } from './ready.js';
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
@@ -24,7 +25,7 @@ Error.stackTraceLimit = Infinity;
|
||||
EventEmitter.defaultMaxListeners = 128;
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
|
||||
const clusterLogger = logger.createSubLogger('cluster', 'orange');
|
||||
const ev = new Xev();
|
||||
|
||||
//#region Events
|
||||
@@ -79,6 +80,8 @@ if (cluster.isWorker || envOption.disableClustering) {
|
||||
await workerMain();
|
||||
}
|
||||
|
||||
readyRef.value = true;
|
||||
|
||||
// ユニットテスト時にMisskeyが子プロセスで起動された時のため
|
||||
// それ以外のときは process.send は使えないので弾く
|
||||
if (process.send) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import * as os from 'node:os';
|
||||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import chalkTemplate from 'chalk-template';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||
import Logger from '@/logger.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -23,7 +25,7 @@ const _dirname = dirname(_filename);
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta');
|
||||
|
||||
const themeColor = chalk.hex('#86b300');
|
||||
|
||||
@@ -71,6 +73,24 @@ export async function masterMain() {
|
||||
|
||||
bootLogger.succ('Misskey initialized');
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
],
|
||||
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.sentryForBackend.options,
|
||||
});
|
||||
}
|
||||
|
||||
if (envOption.disableClustering) {
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const fallback = Symbol('fallback');
|
||||
export const readyRef = { value: false };
|
||||
@@ -4,13 +4,36 @@
|
||||
*/
|
||||
|
||||
import cluster from 'node:cluster';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||
import { envOption } from '@/env.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { jobQueue, server } from './common.js';
|
||||
|
||||
/**
|
||||
* Init worker process
|
||||
*/
|
||||
export async function workerMain() {
|
||||
const config = loadConfig();
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
],
|
||||
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.sentryForBackend.options,
|
||||
});
|
||||
}
|
||||
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
} else if (envOption.onlyQueue) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
|
||||
type RedisOptionsSource = Partial<RedisOptions> & {
|
||||
@@ -56,6 +57,10 @@ type Source = {
|
||||
index: string;
|
||||
scope?: 'local' | 'global' | string[];
|
||||
};
|
||||
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
|
||||
sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };
|
||||
|
||||
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
|
||||
|
||||
proxy?: string;
|
||||
proxySmtp?: string;
|
||||
@@ -145,6 +150,7 @@ export type Config = {
|
||||
signToActivityPubGet: boolean | undefined;
|
||||
|
||||
version: string;
|
||||
publishTarballInsteadOfProvideRepositoryUrl: boolean;
|
||||
host: string;
|
||||
hostname: string;
|
||||
scheme: string;
|
||||
@@ -163,6 +169,8 @@ export type Config = {
|
||||
redisForPubsub: RedisOptions & RedisOptionsSource;
|
||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
||||
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
|
||||
perChannelMaxNoteCacheCount: number;
|
||||
perUserNotificationsMaxCount: number;
|
||||
deactivateAntennaThreshold: number;
|
||||
@@ -209,6 +217,7 @@ export function loadConfig(): Config {
|
||||
|
||||
return {
|
||||
version,
|
||||
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
|
||||
url: url.origin,
|
||||
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
|
||||
socket: config.socket,
|
||||
@@ -230,6 +239,8 @@ export function loadConfig(): Config {
|
||||
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
|
||||
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
||||
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
||||
sentryForBackend: config.sentryForBackend,
|
||||
sentryForFrontend: config.sentryForFrontend,
|
||||
id: config.id,
|
||||
proxy: config.proxy,
|
||||
proxySmtp: config.proxySmtp,
|
||||
|
||||
405
packages/backend/src/core/AbuseReportNotificationService.ts
Normal file
405
packages/backend/src/core/AbuseReportNotificationService.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
|
||||
import { Brackets, In, IsNull, Not } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type {
|
||||
AbuseReportNotificationRecipientRepository,
|
||||
MiAbuseReportNotificationRecipient,
|
||||
MiAbuseUserReport,
|
||||
MiUser,
|
||||
} from '@/models/_.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { IdService } from './IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@Inject(DI.abuseReportNotificationRecipientRepository)
|
||||
private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
private idService: IdService,
|
||||
private roleService: RoleService,
|
||||
private systemWebhookService: SystemWebhookService,
|
||||
private emailService: EmailService,
|
||||
private metaService: MetaService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||
* 通知先ユーザは{@link RoleService.getModeratorIds}の取得結果に依る.
|
||||
*
|
||||
* @see RoleService.getModeratorIds
|
||||
* @see GlobalEventService.publishAdminStream
|
||||
*/
|
||||
@bindThis
|
||||
public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
|
||||
if (abuseReports.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moderatorIds = await this.roleService.getModeratorIds(true, true);
|
||||
|
||||
for (const moderatorId of moderatorIds) {
|
||||
for (const abuseReport of abuseReports) {
|
||||
this.globalEventService.publishAdminStream(
|
||||
moderatorId,
|
||||
'newAbuseUserReport',
|
||||
{
|
||||
id: abuseReport.id,
|
||||
targetUserId: abuseReport.targetUserId,
|
||||
reporterId: abuseReport.reporterId,
|
||||
comment: abuseReport.comment,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||
* メールアドレスの送信先は以下の通り.
|
||||
* - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
|
||||
* - metaテーブルに設定されているメールアドレス
|
||||
*
|
||||
* @see EmailService.sendEmail
|
||||
*/
|
||||
@bindThis
|
||||
public async notifyMail(abuseReports: MiAbuseUserReport[]) {
|
||||
if (abuseReports.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
|
||||
.filter(it => it.isActive && it.userProfile?.emailVerified)
|
||||
.map(it => it.userProfile?.email)
|
||||
.filter(x => x != null),
|
||||
);
|
||||
|
||||
// 送信先の鮮度を保つため、毎回取得する
|
||||
const meta = await this.metaService.fetch(true);
|
||||
recipientEMailAddresses.push(
|
||||
...(meta.email ? [meta.email] : []),
|
||||
);
|
||||
|
||||
if (recipientEMailAddresses.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const mailAddress of recipientEMailAddresses) {
|
||||
await Promise.all(
|
||||
abuseReports.map(it => {
|
||||
// TODO: 送信処理はJobQueue化したい
|
||||
return this.emailService.sendEmail(
|
||||
mailAddress,
|
||||
'New Abuse Report',
|
||||
sanitizeHtml(it.comment),
|
||||
sanitizeHtml(it.comment),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
|
||||
* ここではJobQueueへのエンキューのみを行うため、即時実行されない.
|
||||
*
|
||||
* @see SystemWebhookService.enqueueSystemWebhook
|
||||
*/
|
||||
@bindThis
|
||||
public async notifySystemWebhook(
|
||||
abuseReports: MiAbuseUserReport[],
|
||||
type: 'abuseReport' | 'abuseReportResolved',
|
||||
) {
|
||||
if (abuseReports.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientWebhookIds = await this.fetchWebhookRecipients()
|
||||
.then(it => it
|
||||
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
|
||||
.map(it => it.systemWebhookId)
|
||||
.filter(x => x != null));
|
||||
for (const webhookId of recipientWebhookIds) {
|
||||
await Promise.all(
|
||||
abuseReports.map(it => {
|
||||
return this.systemWebhookService.enqueueSystemWebhook(
|
||||
webhookId,
|
||||
type,
|
||||
it,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通報の通知先一覧を取得する.
|
||||
*
|
||||
* @param {Object} [params] クエリの取得条件
|
||||
* @param {Object} [params.method] 取得する通知先の通知方法
|
||||
* @param {Object} [opts] 動作時の詳細なオプション
|
||||
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
|
||||
* @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
|
||||
* @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
|
||||
* @see removeUnauthorizedRecipientUsers
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchRecipients(
|
||||
params?: {
|
||||
ids?: MiAbuseReportNotificationRecipient['id'][],
|
||||
method?: RecipientMethod[],
|
||||
},
|
||||
opts?: {
|
||||
removeUnauthorized?: boolean,
|
||||
joinUser?: boolean,
|
||||
joinSystemWebhook?: boolean,
|
||||
},
|
||||
): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||
const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
|
||||
|
||||
if (opts?.joinUser) {
|
||||
query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
|
||||
query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
|
||||
}
|
||||
|
||||
if (opts?.joinSystemWebhook) {
|
||||
query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
|
||||
}
|
||||
|
||||
if (params?.ids) {
|
||||
query.andWhere({ id: In(params.ids) });
|
||||
}
|
||||
|
||||
if (params?.method) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
if (params.method?.includes('email')) {
|
||||
qb.orWhere({ method: 'email', userId: Not(IsNull()) });
|
||||
}
|
||||
if (params.method?.includes('webhook')) {
|
||||
qb.orWhere({ method: 'webhook', userId: IsNull() });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const recipients = await query.getMany();
|
||||
if (recipients.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
|
||||
return (opts?.removeUnauthorized ?? true)
|
||||
? await this.removeUnauthorizedRecipientUsers(recipients)
|
||||
: recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* EMailの通知先一覧を取得する.
|
||||
* リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
|
||||
*
|
||||
* @param {Object} [opts]
|
||||
* @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
|
||||
* @see removeUnauthorizedRecipientUsers
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchEMailRecipients(opts?: {
|
||||
removeUnauthorized?: boolean
|
||||
}): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||
return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhookの通知先一覧を取得する.
|
||||
* リレーション先の{@link MiSystemWebhook}も同時に取得する.
|
||||
*/
|
||||
@bindThis
|
||||
public fetchWebhookRecipients(): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||
return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知先を作成する.
|
||||
*/
|
||||
@bindThis
|
||||
public async createRecipient(
|
||||
params: {
|
||||
isActive: MiAbuseReportNotificationRecipient['isActive'];
|
||||
name: MiAbuseReportNotificationRecipient['name'];
|
||||
method: MiAbuseReportNotificationRecipient['method'];
|
||||
userId: MiAbuseReportNotificationRecipient['userId'];
|
||||
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
|
||||
},
|
||||
updater: MiUser,
|
||||
): Promise<MiAbuseReportNotificationRecipient> {
|
||||
const id = this.idService.gen();
|
||||
await this.abuseReportNotificationRecipientRepository.insert({
|
||||
...params,
|
||||
id,
|
||||
});
|
||||
|
||||
const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
|
||||
|
||||
this.moderationLogService
|
||||
.log(updater, 'createAbuseReportNotificationRecipient', {
|
||||
recipientId: id,
|
||||
recipient: created,
|
||||
})
|
||||
.then();
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知先を更新する.
|
||||
*/
|
||||
@bindThis
|
||||
public async updateRecipient(
|
||||
params: {
|
||||
id: MiAbuseReportNotificationRecipient['id'];
|
||||
isActive: MiAbuseReportNotificationRecipient['isActive'];
|
||||
name: MiAbuseReportNotificationRecipient['name'];
|
||||
method: MiAbuseReportNotificationRecipient['method'];
|
||||
userId: MiAbuseReportNotificationRecipient['userId'];
|
||||
systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
|
||||
},
|
||||
updater: MiUser,
|
||||
): Promise<MiAbuseReportNotificationRecipient> {
|
||||
const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
|
||||
|
||||
await this.abuseReportNotificationRecipientRepository.update(params.id, {
|
||||
isActive: params.isActive,
|
||||
updatedAt: new Date(),
|
||||
name: params.name,
|
||||
method: params.method,
|
||||
userId: params.userId,
|
||||
systemWebhookId: params.systemWebhookId,
|
||||
});
|
||||
|
||||
const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
|
||||
|
||||
this.moderationLogService
|
||||
.log(updater, 'updateAbuseReportNotificationRecipient', {
|
||||
recipientId: params.id,
|
||||
before: beforeEntity,
|
||||
after: afterEntity,
|
||||
})
|
||||
.then();
|
||||
|
||||
return afterEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知先を削除する.
|
||||
*/
|
||||
@bindThis
|
||||
public async deleteRecipient(
|
||||
id: MiAbuseReportNotificationRecipient['id'],
|
||||
updater: MiUser,
|
||||
) {
|
||||
const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
|
||||
|
||||
await this.abuseReportNotificationRecipientRepository.delete(id);
|
||||
|
||||
this.moderationLogService
|
||||
.log(updater, 'deleteAbuseReportNotificationRecipient', {
|
||||
recipientId: id,
|
||||
recipient: entity,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
/**
|
||||
* モデレータ権限を持たない(*1)通知先ユーザを削除する.
|
||||
*
|
||||
* *1: 以下の両方を満たすものの事を言う
|
||||
* - 通知先にユーザIDが設定されている
|
||||
* - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
|
||||
*
|
||||
* @param recipients 通知先一覧の配列
|
||||
* @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
|
||||
*/
|
||||
@bindThis
|
||||
private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
|
||||
const userRecipients = recipients.filter(it => it.userId !== null);
|
||||
const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
|
||||
if (recipientUserIds.size <= 0) {
|
||||
// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
|
||||
return recipients;
|
||||
}
|
||||
|
||||
// モデレータ権限の有無で通知先設定を振り分ける
|
||||
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
|
||||
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||
for (const recipient of userRecipients) {
|
||||
// eslint-disable-next-line
|
||||
if (authorizedUserIds.includes(recipient.userId!)) {
|
||||
authorizedUserRecipients.push(recipient);
|
||||
} else {
|
||||
unauthorizedUserRecipients.push(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
// モデレータ権限を持たない通知先をDBから削除する
|
||||
if (unauthorizedUserRecipients.length > 0) {
|
||||
await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
|
||||
}
|
||||
const nonUserRecipients = recipients.filter(it => it.userId === null);
|
||||
return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
if (obj.channel !== 'internal') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'roleUpdated':
|
||||
case 'roleDeleted':
|
||||
case 'userRoleUnassigned': {
|
||||
// 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
|
||||
process.nextTick(async () => {
|
||||
const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
|
||||
userId: Not(IsNull()),
|
||||
});
|
||||
await this.removeUnauthorizedRecipientUsers(recipients);
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
128
packages/backend/src/core/AbuseReportService.ts
Normal file
128
packages/backend/src/core/AbuseReportService.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
|
||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { IdService } from './IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AbuseReportService {
|
||||
constructor(
|
||||
@Inject(DI.abuseUserReportsRepository)
|
||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
private idService: IdService,
|
||||
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||
private queueService: QueueService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private apRendererService: ApRendererService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
|
||||
* - 管理者用Redisイベント
|
||||
* - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス)
|
||||
* - SystemWebhook
|
||||
*
|
||||
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||
* @see AbuseReportNotificationService.notify
|
||||
*/
|
||||
@bindThis
|
||||
public async report(params: {
|
||||
targetUserId: MiAbuseUserReport['targetUserId'],
|
||||
targetUserHost: MiAbuseUserReport['targetUserHost'],
|
||||
reporterId: MiAbuseUserReport['reporterId'],
|
||||
reporterHost: MiAbuseUserReport['reporterHost'],
|
||||
comment: string,
|
||||
}[]) {
|
||||
const entities = params.map(param => {
|
||||
return {
|
||||
id: this.idService.gen(),
|
||||
targetUserId: param.targetUserId,
|
||||
targetUserHost: param.targetUserHost,
|
||||
reporterId: param.reporterId,
|
||||
reporterHost: param.reporterHost,
|
||||
comment: param.comment,
|
||||
};
|
||||
});
|
||||
|
||||
const reports = Array.of<MiAbuseUserReport>();
|
||||
for (const entity of entities) {
|
||||
const report = await this.abuseUserReportsRepository.insertOne(entity);
|
||||
reports.push(report);
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
this.abuseReportNotificationService.notifyAdminStream(reports),
|
||||
this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
|
||||
this.abuseReportNotificationService.notifyMail(reports),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通報を解決し、その内容を下記の手段で管理者各位に通知する.
|
||||
* - SystemWebhook
|
||||
*
|
||||
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||
* @param operator 通報を処理したユーザ
|
||||
* @see AbuseReportNotificationService.notify
|
||||
*/
|
||||
@bindThis
|
||||
public async resolve(
|
||||
params: {
|
||||
reportId: string;
|
||||
forward: boolean;
|
||||
}[],
|
||||
operator: MiUser,
|
||||
) {
|
||||
const paramsMap = new Map(params.map(it => [it.reportId, it]));
|
||||
const reports = await this.abuseUserReportsRepository.findBy({
|
||||
id: In(params.map(it => it.reportId)),
|
||||
});
|
||||
|
||||
for (const report of reports) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const ps = paramsMap.get(report.id)!;
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
resolved: true,
|
||||
assigneeId: operator.id,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
});
|
||||
|
||||
if (ps.forward && report.targetUserHost != null) {
|
||||
const actor = await this.instanceActorService.getInstanceActor();
|
||||
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
||||
|
||||
// eslint-disable-next-line
|
||||
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
|
||||
const contextAssignedFlag = this.apRendererService.addContext(flag);
|
||||
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
|
||||
}
|
||||
|
||||
this.moderationLogService
|
||||
.log(operator, 'resolveAbuseReport', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
|
||||
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
@@ -60,7 +59,6 @@ export class AccountMoveService {
|
||||
private instanceChart: InstanceChart,
|
||||
private metaService: MetaService,
|
||||
private relayService: RelayService,
|
||||
private cacheService: CacheService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
@@ -84,7 +82,7 @@ export class AccountMoveService {
|
||||
Object.assign(src, update);
|
||||
|
||||
// Update cache
|
||||
this.cacheService.uriPersonCache.set(srcUri, src);
|
||||
this.globalEventService.publishInternalEvent('localUserUpdated', src);
|
||||
|
||||
const srcPerson = await this.apRendererService.renderPerson(src);
|
||||
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
|
||||
@@ -307,7 +305,7 @@ export class AccountMoveService {
|
||||
let resultUser: MiLocalUser | MiRemoteUser | null = null;
|
||||
|
||||
if (this.userEntityService.isRemoteUser(dst)) {
|
||||
if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||
if (Date.now() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||
await this.apPersonService.updatePerson(dst.uri);
|
||||
}
|
||||
dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst;
|
||||
@@ -323,7 +321,7 @@ export class AccountMoveService {
|
||||
if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー
|
||||
|
||||
if (this.userEntityService.isRemoteUser(dst)) {
|
||||
if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||
if (Date.now() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||
await this.apPersonService.updatePerson(srcUri);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Brackets, EntityNotFoundError } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
@@ -29,6 +30,7 @@ export class AnnouncementService {
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private announcementEntityService: AnnouncementEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -65,7 +67,7 @@ export class AnnouncementService {
|
||||
|
||||
@bindThis
|
||||
public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
|
||||
const announcement = await this.announcementsRepository.insert({
|
||||
const announcement = await this.announcementsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: null,
|
||||
title: values.title,
|
||||
@@ -77,9 +79,9 @@ export class AnnouncementService {
|
||||
silence: values.silence,
|
||||
needConfirmationToRead: values.needConfirmationToRead,
|
||||
userId: values.userId,
|
||||
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
const packed = (await this.packMany([announcement]))[0];
|
||||
const packed = await this.announcementEntityService.pack(announcement);
|
||||
|
||||
if (values.userId) {
|
||||
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
|
||||
@@ -177,6 +179,24 @@ export class AnnouncementService {
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise<Packed<'Announcement'>> {
|
||||
const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId });
|
||||
if (me) {
|
||||
if (announcement.userId && announcement.userId !== me.id) {
|
||||
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
|
||||
}
|
||||
|
||||
const read = await this.announcementReadsRepository.findOneBy({
|
||||
announcementId: announcement.id,
|
||||
userId: me.id,
|
||||
});
|
||||
return this.announcementEntityService.pack({ ...announcement, isRead: read !== null }, me);
|
||||
} else {
|
||||
return this.announcementEntityService.pack(announcement, null);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> {
|
||||
try {
|
||||
@@ -193,29 +213,4 @@ export class AnnouncementService {
|
||||
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMany(
|
||||
announcements: MiAnnouncement[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
options?: {
|
||||
reads?: MiAnnouncementRead[];
|
||||
},
|
||||
): Promise<Packed<'Announcement'>[]> {
|
||||
const reads = me ? (options?.reads ?? await this.getReads(me.id)) : [];
|
||||
return announcements.map(announcement => ({
|
||||
id: announcement.id,
|
||||
createdAt: this.idService.parse(announcement.id).date.toISOString(),
|
||||
updatedAt: announcement.updatedAt?.toISOString() ?? null,
|
||||
text: announcement.text,
|
||||
title: announcement.title,
|
||||
imageUrl: announcement.imageUrl,
|
||||
icon: announcement.icon,
|
||||
display: announcement.display,
|
||||
needConfirmationToRead: announcement.needConfirmationToRead,
|
||||
silence: announcement.silence,
|
||||
forYou: announcement.userId === me?.id,
|
||||
isRead: reads.some(read => read.announcementId === announcement.id),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise<void> {
|
||||
public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<void> {
|
||||
const antennas = await this.getAntennas();
|
||||
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
||||
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
||||
@@ -110,10 +110,12 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
|
||||
@bindThis
|
||||
public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise<boolean> {
|
||||
public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<boolean> {
|
||||
if (note.visibility === 'specified') return false;
|
||||
if (note.visibility === 'followers') return false;
|
||||
|
||||
if (antenna.excludeBots && noteUser.isBot) return false;
|
||||
|
||||
if (antenna.localOnly && noteUser.host != null) return false;
|
||||
|
||||
if (!antenna.withReplies && note.replyId != null) return false;
|
||||
|
||||
@@ -55,10 +55,10 @@ export class AvatarDecorationService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
|
||||
const created = await this.avatarDecorationsRepository.insert({
|
||||
const created = await this.avatarDecorationsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
...options,
|
||||
}).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CacheService implements OnApplicationShutdown {
|
||||
public userByIdCache: MemoryKVCache<MiUser, MiUser | string>;
|
||||
public localUserByNativeTokenCache: MemoryKVCache<MiLocalUser | null, string | null>;
|
||||
public userByIdCache: MemoryKVCache<MiUser>;
|
||||
public localUserByNativeTokenCache: MemoryKVCache<MiLocalUser | null>;
|
||||
public localUserByIdCache: MemoryKVCache<MiLocalUser>;
|
||||
public uriPersonCache: MemoryKVCache<MiUser | null, string | null>;
|
||||
public uriPersonCache: MemoryKVCache<MiUser | null>;
|
||||
public userProfileCache: RedisKVCache<MiUserProfile>;
|
||||
public userMutingsCache: RedisKVCache<Set<string>>;
|
||||
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||
@@ -56,41 +56,10 @@ export class CacheService implements OnApplicationShutdown {
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
||||
const localUserByIdCache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 60 * 6 /* 6h */);
|
||||
this.localUserByIdCache = localUserByIdCache;
|
||||
|
||||
// ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する
|
||||
const userByIdCache = new MemoryKVCache<MiUser, MiUser | string>(1000 * 60 * 60 * 6 /* 6h */, {
|
||||
toMapConverter: user => {
|
||||
if (user.host === null) {
|
||||
localUserByIdCache.set(user.id, user as MiLocalUser);
|
||||
return user.id;
|
||||
}
|
||||
|
||||
return user;
|
||||
},
|
||||
fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId,
|
||||
});
|
||||
this.userByIdCache = userByIdCache;
|
||||
|
||||
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null, string | null>(Infinity, {
|
||||
toMapConverter: user => {
|
||||
if (user === null) return null;
|
||||
|
||||
localUserByIdCache.set(user.id, user);
|
||||
return user.id;
|
||||
},
|
||||
fromMapConverter: id => id === null ? null : localUserByIdCache.get(id),
|
||||
});
|
||||
this.uriPersonCache = new MemoryKVCache<MiUser | null, string | null>(Infinity, {
|
||||
toMapConverter: user => {
|
||||
if (user === null) return null;
|
||||
|
||||
userByIdCache.set(user.id, user);
|
||||
return user.id;
|
||||
},
|
||||
fromMapConverter: id => id === null ? null : userByIdCache.get(id),
|
||||
});
|
||||
this.userByIdCache = new MemoryKVCache<MiUser>(Infinity);
|
||||
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity);
|
||||
this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity);
|
||||
this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity);
|
||||
|
||||
this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
@@ -159,17 +128,29 @@ export class CacheService implements OnApplicationShutdown {
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'userChangeSuspendedState':
|
||||
case 'remoteUserUpdated': {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
|
||||
this.userByIdCache.set(user.id, user);
|
||||
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
||||
if (v.value === user.id) {
|
||||
this.uriPersonCache.set(k, user);
|
||||
case 'userChangeDeletedState':
|
||||
case 'remoteUserUpdated':
|
||||
case 'localUserUpdated': {
|
||||
const user = await this.usersRepository.findOneBy({ id: body.id });
|
||||
if (user == null) {
|
||||
this.userByIdCache.delete(body.id);
|
||||
this.localUserByIdCache.delete(body.id);
|
||||
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
||||
if (v.value?.id === body.id) {
|
||||
this.uriPersonCache.delete(k);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.userByIdCache.set(user.id, user);
|
||||
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
||||
if (v.value?.id === user.id) {
|
||||
this.uriPersonCache.set(k, user);
|
||||
}
|
||||
}
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
this.localUserByNativeTokenCache.set(user.token!, user);
|
||||
this.localUserByIdCache.set(user.id, user);
|
||||
}
|
||||
}
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
this.localUserByNativeTokenCache.set(user.token!, user);
|
||||
this.localUserByIdCache.set(user.id, user);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
@@ -41,17 +41,17 @@ export class ClipService {
|
||||
const currentCount = await this.clipsRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
|
||||
throw new ClipService.TooManyClipsError();
|
||||
}
|
||||
|
||||
const clip = await this.clipsRepository.insert({
|
||||
const clip = await this.clipsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
userId: me.id,
|
||||
name: name,
|
||||
isPublic: isPublic,
|
||||
description: description,
|
||||
}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
return clip;
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export class ClipService {
|
||||
const currentCount = await this.clipNotesRepository.countBy({
|
||||
clipId: clip.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
|
||||
throw new ClipService.TooManyClipNotesError();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
|
||||
import {
|
||||
AbuseReportNotificationRecipientEntityService,
|
||||
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
|
||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { AccountMoveService } from './AccountMoveService.js';
|
||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AiService } from './AiService.js';
|
||||
@@ -56,7 +63,7 @@ import { UserMutingService } from './UserMutingService.js';
|
||||
import { UserSuspendService } from './UserSuspendService.js';
|
||||
import { UserAuthService } from './UserAuthService.js';
|
||||
import { VideoProcessingService } from './VideoProcessingService.js';
|
||||
import { WebhookService } from './WebhookService.js';
|
||||
import { UserWebhookService } from './UserWebhookService.js';
|
||||
import { ProxyAccountService } from './ProxyAccountService.js';
|
||||
import { UtilityService } from './UtilityService.js';
|
||||
import { FileInfoService } from './FileInfoService.js';
|
||||
@@ -85,6 +92,7 @@ import ApRequestChart from './chart/charts/ap-request.js';
|
||||
import { ChartManagementService } from './chart/ChartManagementService.js';
|
||||
|
||||
import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js';
|
||||
import { AnnouncementEntityService } from './entities/AnnouncementEntityService.js';
|
||||
import { AntennaEntityService } from './entities/AntennaEntityService.js';
|
||||
import { AppEntityService } from './entities/AppEntityService.js';
|
||||
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
|
||||
@@ -117,6 +125,7 @@ import { FlashEntityService } from './entities/FlashEntityService.js';
|
||||
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
|
||||
import { RoleEntityService } from './entities/RoleEntityService.js';
|
||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
import { MetaEntityService } from './entities/MetaEntityService.js';
|
||||
|
||||
import { ApAudienceService } from './activitypub/ApAudienceService.js';
|
||||
import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
|
||||
@@ -127,7 +136,7 @@ import { ApMfmService } from './activitypub/ApMfmService.js';
|
||||
import { ApRendererService } from './activitypub/ApRendererService.js';
|
||||
import { ApRequestService } from './activitypub/ApRequestService.js';
|
||||
import { ApResolverService } from './activitypub/ApResolverService.js';
|
||||
import { LdSignatureService } from './activitypub/LdSignatureService.js';
|
||||
import { JsonLdService } from './activitypub/JsonLdService.js';
|
||||
import { RemoteLoggerService } from './RemoteLoggerService.js';
|
||||
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
|
||||
import { WebfingerService } from './WebfingerService.js';
|
||||
@@ -143,6 +152,8 @@ import type { Provider } from '@nestjs/common';
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
|
||||
const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
|
||||
const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
|
||||
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
|
||||
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
|
||||
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
|
||||
@@ -195,7 +206,8 @@ const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting
|
||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
|
||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||
@@ -224,6 +236,8 @@ const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRe
|
||||
const $ChartManagementService: Provider = { provide: 'ChartManagementService', useExisting: ChartManagementService };
|
||||
|
||||
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
|
||||
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
|
||||
const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
|
||||
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
|
||||
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
|
||||
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
|
||||
@@ -256,6 +270,8 @@ const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisti
|
||||
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
|
||||
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
|
||||
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
||||
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
|
||||
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
|
||||
|
||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
@@ -266,7 +282,7 @@ const $ApMfmService: Provider = { provide: 'ApMfmService', useExisting: ApMfmSer
|
||||
const $ApRendererService: Provider = { provide: 'ApRendererService', useExisting: ApRendererService };
|
||||
const $ApRequestService: Provider = { provide: 'ApRequestService', useExisting: ApRequestService };
|
||||
const $ApResolverService: Provider = { provide: 'ApResolverService', useExisting: ApResolverService };
|
||||
const $LdSignatureService: Provider = { provide: 'LdSignatureService', useExisting: LdSignatureService };
|
||||
const $JsonLdService: Provider = { provide: 'JsonLdService', useExisting: JsonLdService };
|
||||
const $RemoteLoggerService: Provider = { provide: 'RemoteLoggerService', useExisting: RemoteLoggerService };
|
||||
const $RemoteUserResolveService: Provider = { provide: 'RemoteUserResolveService', useExisting: RemoteUserResolveService };
|
||||
const $WebfingerService: Provider = { provide: 'WebfingerService', useExisting: WebfingerService };
|
||||
@@ -283,6 +299,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
AbuseReportService,
|
||||
AbuseReportNotificationService,
|
||||
AccountMoveService,
|
||||
AccountUpdateService,
|
||||
AiService,
|
||||
@@ -335,7 +353,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
WebhookService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
@@ -364,6 +383,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ChartManagementService,
|
||||
|
||||
AbuseUserReportEntityService,
|
||||
AnnouncementEntityService,
|
||||
AbuseReportNotificationRecipientEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
AuthSessionEntityService,
|
||||
@@ -396,6 +417,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -406,7 +429,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ApRendererService,
|
||||
ApRequestService,
|
||||
ApResolverService,
|
||||
LdSignatureService,
|
||||
JsonLdService,
|
||||
RemoteLoggerService,
|
||||
RemoteUserResolveService,
|
||||
WebfingerService,
|
||||
@@ -419,6 +442,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
$AbuseReportService,
|
||||
$AbuseReportNotificationService,
|
||||
$AccountMoveService,
|
||||
$AccountUpdateService,
|
||||
$AiService,
|
||||
@@ -471,7 +496,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
$WebhookService,
|
||||
$UserWebhookService,
|
||||
$SystemWebhookService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
@@ -500,6 +526,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ChartManagementService,
|
||||
|
||||
$AbuseUserReportEntityService,
|
||||
$AnnouncementEntityService,
|
||||
$AbuseReportNotificationRecipientEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
$AuthSessionEntityService,
|
||||
@@ -532,6 +560,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
@@ -542,7 +572,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ApRendererService,
|
||||
$ApRequestService,
|
||||
$ApResolverService,
|
||||
$LdSignatureService,
|
||||
$JsonLdService,
|
||||
$RemoteLoggerService,
|
||||
$RemoteUserResolveService,
|
||||
$WebfingerService,
|
||||
@@ -556,6 +586,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
exports: [
|
||||
QueueModule,
|
||||
LoggerService,
|
||||
AbuseReportService,
|
||||
AbuseReportNotificationService,
|
||||
AccountMoveService,
|
||||
AccountUpdateService,
|
||||
AiService,
|
||||
@@ -608,7 +640,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
UserSuspendService,
|
||||
UserAuthService,
|
||||
VideoProcessingService,
|
||||
WebhookService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
@@ -636,6 +669,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ChartManagementService,
|
||||
|
||||
AbuseUserReportEntityService,
|
||||
AnnouncementEntityService,
|
||||
AbuseReportNotificationRecipientEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
AuthSessionEntityService,
|
||||
@@ -668,6 +703,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -678,7 +715,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ApRendererService,
|
||||
ApRequestService,
|
||||
ApResolverService,
|
||||
LdSignatureService,
|
||||
JsonLdService,
|
||||
RemoteLoggerService,
|
||||
RemoteUserResolveService,
|
||||
WebfingerService,
|
||||
@@ -691,6 +728,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
$AbuseReportService,
|
||||
$AbuseReportNotificationService,
|
||||
$AccountMoveService,
|
||||
$AccountUpdateService,
|
||||
$AiService,
|
||||
@@ -743,7 +782,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$UserSuspendService,
|
||||
$UserAuthService,
|
||||
$VideoProcessingService,
|
||||
$WebhookService,
|
||||
$UserWebhookService,
|
||||
$SystemWebhookService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
@@ -771,6 +811,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ChartManagementService,
|
||||
|
||||
$AbuseUserReportEntityService,
|
||||
$AnnouncementEntityService,
|
||||
$AbuseReportNotificationRecipientEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
$AuthSessionEntityService,
|
||||
@@ -803,6 +845,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
@@ -813,7 +857,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ApRendererService,
|
||||
$ApRequestService,
|
||||
$ApResolverService,
|
||||
$LdSignatureService,
|
||||
$JsonLdService,
|
||||
$RemoteLoggerService,
|
||||
$RemoteUserResolveService,
|
||||
$WebfingerService,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { query } from '@/misc/prelude/url.js';
|
||||
import type { Serialized } from '@/types.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||
const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
|
||||
|
||||
@Injectable()
|
||||
export class CustomEmojiService implements OnApplicationShutdown {
|
||||
@@ -68,7 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||
localOnly: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
|
||||
}, moderator?: MiUser): Promise<MiEmoji> {
|
||||
const emoji = await this.emojisRepository.insert({
|
||||
const emoji = await this.emojisRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: data.name,
|
||||
@@ -82,7 +82,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
if (data.host == null) {
|
||||
this.localEmojisCache.refresh();
|
||||
@@ -346,10 +346,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<Record<string, string>> {
|
||||
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
|
||||
const res = {} as any;
|
||||
const res = {} as Record<string, string>;
|
||||
for (let i = 0; i < emojiNames.length; i++) {
|
||||
if (emojis[i] != null) {
|
||||
res[emojiNames[i]] = emojis[i];
|
||||
const resolvedEmoji = emojis[i];
|
||||
if (resolvedEmoji != null) {
|
||||
res[emojiNames[i]] = resolvedEmoji;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
@@ -393,6 +394,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||
return this.emojisRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getEmojiByName(name: string): Promise<MiEmoji | null> {
|
||||
return this.emojisRepository.findOneBy({ name, host: IsNull() });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.cache.dispose();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { QueueService } from '@/core/QueueService.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAccountService {
|
||||
@@ -18,6 +19,7 @@ export class DeleteAccountService {
|
||||
|
||||
private userSuspendService: UserSuspendService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -39,5 +41,7 @@ export class DeleteAccountService {
|
||||
await this.usersRepository.update(user.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ export class DriveService {
|
||||
file.size = size;
|
||||
file.storedInternal = false;
|
||||
|
||||
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
return await this.driveFilesRepository.insertOne(file);
|
||||
} else { // use internal storage
|
||||
const accessKey = randomUUID();
|
||||
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
|
||||
@@ -254,7 +254,7 @@ export class DriveService {
|
||||
file.md5 = hash;
|
||||
file.size = size;
|
||||
|
||||
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
return await this.driveFilesRepository.insertOne(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,14 +497,20 @@ export class DriveService {
|
||||
|
||||
if (user && !force) {
|
||||
// Check if there is a file with the same hash
|
||||
const much = await this.driveFilesRepository.findOneBy({
|
||||
const matched = await this.driveFilesRepository.findOneBy({
|
||||
md5: info.md5,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (much) {
|
||||
this.registerLogger.info(`file with same hash is found: ${much.id}`);
|
||||
return much;
|
||||
if (matched) {
|
||||
this.registerLogger.info(`file with same hash is found: ${matched.id}`);
|
||||
if (sensitive && !matched.isSensitive) {
|
||||
// The file is federated as sensitive for this time, but was federated as non-sensitive before.
|
||||
// Therefore, update the file to sensitive.
|
||||
await this.driveFilesRepository.update({ id: matched.id }, { isSensitive: true });
|
||||
matched.isSensitive = true;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,7 +615,7 @@ export class DriveService {
|
||||
file.type = info.type.mime;
|
||||
file.storedInternal = false;
|
||||
|
||||
file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
file = await this.driveFilesRepository.insertOne(file);
|
||||
} catch (err) {
|
||||
// duplicate key error (when already registered)
|
||||
if (isDuplicateKeyValueError(err)) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
@@ -32,6 +33,7 @@ export class EmailService {
|
||||
private loggerService: LoggerService,
|
||||
private utilityService: UtilityService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('email');
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { NotesRepository } from '@/models/_.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
@@ -55,24 +55,20 @@ export class FanoutTimelineEndpointService {
|
||||
|
||||
@bindThis
|
||||
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
|
||||
let noteIds: string[];
|
||||
let shouldFallbackToDb = false;
|
||||
|
||||
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
|
||||
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
|
||||
|
||||
const shouldPrepend = ps.sinceId && !ps.untilId;
|
||||
const idCompare: (a: string, b: string) => number = shouldPrepend ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
|
||||
const ascending = ps.sinceId && !ps.untilId;
|
||||
const idCompare: (a: string, b: string) => number = ascending ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
|
||||
|
||||
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
||||
|
||||
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
|
||||
const redisResultIds = Array.from(new Set(redisResult.flat(1)));
|
||||
const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
|
||||
|
||||
redisResultIds.sort(idCompare);
|
||||
noteIds = redisResultIds.slice(0, ps.limit);
|
||||
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
let noteIds = redisResultIds.slice(0, ps.limit);
|
||||
const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
|
||||
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
let filter = ps.noteFilter ?? (_note => true);
|
||||
@@ -95,7 +91,7 @@ export class FanoutTimelineEndpointService {
|
||||
|
||||
if (ps.excludePureRenotes) {
|
||||
const parentFilter = filter;
|
||||
filter = (note) => !isPureRenote(note) && parentFilter(note);
|
||||
filter = (note) => (!isRenote(note) || isQuote(note)) && parentFilter(note);
|
||||
}
|
||||
|
||||
if (ps.me) {
|
||||
@@ -116,7 +112,7 @@ export class FanoutTimelineEndpointService {
|
||||
filter = (note) => {
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
|
||||
if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false;
|
||||
if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
|
||||
if (isInstanceMuted(note, userMutedInstances)) return false;
|
||||
|
||||
return parentFilter(note);
|
||||
@@ -142,9 +138,7 @@ export class FanoutTimelineEndpointService {
|
||||
|
||||
if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) {
|
||||
// 十分Redisからとれた
|
||||
const result = redisTimeline.slice(0, ps.limit);
|
||||
if (shouldPrepend) result.reverse();
|
||||
return result;
|
||||
return redisTimeline.slice(0, ps.limit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,8 +146,7 @@ export class FanoutTimelineEndpointService {
|
||||
const remainingToRead = ps.limit - redisTimeline.length;
|
||||
let dbUntil: string | null;
|
||||
let dbSince: string | null;
|
||||
if (shouldPrepend) {
|
||||
redisTimeline.reverse();
|
||||
if (ascending) {
|
||||
dbUntil = ps.untilId;
|
||||
dbSince = noteIds[noteIds.length - 1];
|
||||
} else {
|
||||
@@ -161,7 +154,7 @@ export class FanoutTimelineEndpointService {
|
||||
dbSince = ps.sinceId;
|
||||
}
|
||||
const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead);
|
||||
return shouldPrepend ? [...gotFromDb, ...redisTimeline] : [...redisTimeline, ...gotFromDb];
|
||||
return [...redisTimeline, ...gotFromDb];
|
||||
}
|
||||
|
||||
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);
|
||||
|
||||
@@ -55,11 +55,11 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
||||
const index = await this.instancesRepository.findOneBy({ host });
|
||||
|
||||
if (index == null) {
|
||||
const i = await this.instancesRepository.insert({
|
||||
const i = await this.instancesRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
host,
|
||||
firstRetrievedAt: new Date(),
|
||||
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
this.federatedInstanceCache.set(host, i);
|
||||
return i;
|
||||
|
||||
@@ -51,21 +51,35 @@ export class FetchInstanceMetadataService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async tryLock(host: string): Promise<boolean> {
|
||||
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
|
||||
return mutex !== '1';
|
||||
// public for test
|
||||
public async tryLock(host: string): Promise<string | null> {
|
||||
// TODO: マイグレーションなのであとで消す (2024.3.1)
|
||||
this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`);
|
||||
|
||||
return await this.redisClient.set(
|
||||
`fetchInstanceMetadata:mutex:v2:${host}`, '1',
|
||||
'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
|
||||
'GET' // 古い値を返す(なかったらnull)
|
||||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public unlock(host: string): Promise<'OK'> {
|
||||
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
|
||||
// public for test
|
||||
public unlock(host: string): Promise<number> {
|
||||
return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
|
||||
const host = instance.host;
|
||||
// Acquire mutex to ensure no parallel runs
|
||||
if (!await this.tryLock(host)) return;
|
||||
|
||||
// finallyでunlockされてしまうのでtry内でロックチェックをしない
|
||||
// (returnであってもfinallyは実行される)
|
||||
if (!force && await this.tryLock(host) === '1') {
|
||||
// 1が返ってきていたらロックされているという意味なので、何もしない
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!force) {
|
||||
const _instance = await this.federatedInstanceService.fetch(host);
|
||||
@@ -140,7 +154,7 @@ export class FetchInstanceMetadataService {
|
||||
throw new Error('No wellknown links');
|
||||
}
|
||||
|
||||
const links = wellknown.links as any[];
|
||||
const links = wellknown.links as ({ rel: string, href: string; })[];
|
||||
|
||||
const link1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
|
||||
const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
|
||||
|
||||
@@ -14,10 +14,12 @@ 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 { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||
import { encode } from 'blurhash';
|
||||
import { createTempDir } from '@/misc/create-temp.js';
|
||||
import { AiService } from '@/core/AiService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
export type FileInfo = {
|
||||
@@ -48,9 +50,13 @@ const TYPE_SVG = {
|
||||
|
||||
@Injectable()
|
||||
export class FileInfoService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
private aiService: AiService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('file-info');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +128,7 @@ export class FileInfoService {
|
||||
'image/avif',
|
||||
'image/svg+xml',
|
||||
].includes(type.mime)) {
|
||||
blurhash = await this.getBlurhash(path).catch(e => {
|
||||
blurhash = await this.getBlurhash(path, type.mime).catch(e => {
|
||||
warnings.push(`getBlurhash failed: ${e}`);
|
||||
return undefined;
|
||||
});
|
||||
@@ -316,6 +322,34 @@ export class FileInfoService {
|
||||
return mime;
|
||||
}
|
||||
|
||||
/**
|
||||
* ビデオファイルにビデオトラックがあるかどうかチェック
|
||||
* (ない場合:m4a, webmなど)
|
||||
*
|
||||
* @param path ファイルパス
|
||||
* @returns ビデオトラックがあるかどうか(エラー発生時は常に`true`を返す)
|
||||
*/
|
||||
@bindThis
|
||||
private hasVideoTrackOnVideoFile(path: string): Promise<boolean> {
|
||||
const sublogger = this.logger.createSubLogger('ffprobe');
|
||||
sublogger.info(`Checking the video file. File path: ${path}`);
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
FFmpeg.ffprobe(path, (err, metadata) => {
|
||||
if (err) {
|
||||
sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
resolve(metadata.streams.some((stream) => stream.codec_type === 'video'));
|
||||
});
|
||||
} catch (err) {
|
||||
sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error);
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MIME Type and extension
|
||||
*/
|
||||
@@ -338,6 +372,20 @@ export class FileInfoService {
|
||||
return TYPE_SVG;
|
||||
}
|
||||
|
||||
if ((type.mime.startsWith('video') || type.mime === 'application/ogg') && !(await this.hasVideoTrackOnVideoFile(path))) {
|
||||
const newMime = `audio/${type.mime.split('/')[1]}`;
|
||||
if (newMime === 'audio/mp4') {
|
||||
return {
|
||||
mime: 'audio/mp4',
|
||||
ext: 'm4a',
|
||||
};
|
||||
}
|
||||
return {
|
||||
mime: newMime,
|
||||
ext: type.ext,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mime: this.fixMime(type.mime),
|
||||
ext: type.ext,
|
||||
@@ -407,9 +455,9 @@ export class FileInfoService {
|
||||
* Calculate average color of image
|
||||
*/
|
||||
@bindThis
|
||||
private getBlurhash(path: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
sharp(path)
|
||||
private getBlurhash(path: string, type: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
(await sharpBmp(path, type))
|
||||
.raw()
|
||||
.ensureAlpha()
|
||||
.resize(64, 64, { fit: 'inside' })
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import type { MiSignin } from '@/models/Signin.js';
|
||||
import type { MiPage } from '@/models/Page.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
@@ -70,6 +71,7 @@ export interface MainEventTypes {
|
||||
file: Packed<'DriveFile'>;
|
||||
};
|
||||
readAllNotifications: undefined;
|
||||
notificationFlushed: undefined;
|
||||
unreadNotification: Packed<'Notification'>;
|
||||
unreadMention: MiNote['id'];
|
||||
readAllUnreadMentions: undefined;
|
||||
@@ -256,8 +258,10 @@ type SerializedAll<T> = {
|
||||
|
||||
export interface InternalEventTypes {
|
||||
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
|
||||
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
|
||||
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
|
||||
remoteUserUpdated: { id: MiUser['id']; };
|
||||
localUserUpdated: { id: MiUser['id']; };
|
||||
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
@@ -271,6 +275,9 @@ export interface InternalEventTypes {
|
||||
webhookCreated: MiWebhook;
|
||||
webhookDeleted: MiWebhook;
|
||||
webhookUpdated: MiWebhook;
|
||||
systemWebhookCreated: MiSystemWebhook;
|
||||
systemWebhookDeleted: MiSystemWebhook;
|
||||
systemWebhookUpdated: MiSystemWebhook;
|
||||
antennaCreated: MiAntenna;
|
||||
antennaDeleted: MiAntenna;
|
||||
antennaUpdated: MiAntenna;
|
||||
|
||||
@@ -14,9 +14,16 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
||||
import type { IObject } from '@/core/activitypub/type.js';
|
||||
import type { Response } from 'node-fetch';
|
||||
import type { URL } from 'node:url';
|
||||
|
||||
export type HttpRequestSendOptions = {
|
||||
throwErrorWhenResponseNotOk: boolean;
|
||||
validators?: ((res: Response) => void)[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class HttpRequestService {
|
||||
/**
|
||||
@@ -104,6 +111,23 @@ export class HttpRequestService {
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActivityJson(url: string): Promise<IObject> {
|
||||
const res = await this.send(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
},
|
||||
timeout: 5000,
|
||||
size: 1024 * 256,
|
||||
}, {
|
||||
throwErrorWhenResponseNotOk: true,
|
||||
validators: [validateContentTypeSetAsActivityPub],
|
||||
});
|
||||
|
||||
return await res.json() as IObject;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
|
||||
const res = await this.send(url, {
|
||||
@@ -132,17 +156,20 @@ export class HttpRequestService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async send(url: string, args: {
|
||||
method?: string,
|
||||
body?: string,
|
||||
headers?: Record<string, string>,
|
||||
timeout?: number,
|
||||
size?: number,
|
||||
} = {}, extra: {
|
||||
throwErrorWhenResponseNotOk: boolean;
|
||||
} = {
|
||||
throwErrorWhenResponseNotOk: true,
|
||||
}): Promise<Response> {
|
||||
public async send(
|
||||
url: string,
|
||||
args: {
|
||||
method?: string,
|
||||
body?: string,
|
||||
headers?: Record<string, string>,
|
||||
timeout?: number,
|
||||
size?: number,
|
||||
} = {},
|
||||
extra: HttpRequestSendOptions = {
|
||||
throwErrorWhenResponseNotOk: true,
|
||||
validators: [],
|
||||
},
|
||||
): Promise<Response> {
|
||||
const timeout = args.timeout ?? 5000;
|
||||
|
||||
const controller = new AbortController();
|
||||
@@ -166,6 +193,12 @@ export class HttpRequestService {
|
||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
for (const validator of (extra.validators ?? [])) {
|
||||
validator(res);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export class LoggerService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
|
||||
return new Logger(domain, color, store);
|
||||
public getLogger(domain: string, color?: KEYWORD | undefined) {
|
||||
return new Logger(domain, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as parse5 from 'parse5';
|
||||
import { Window } from 'happy-dom';
|
||||
import { Window, XMLSerializer } from 'happy-dom';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { intersperse } from '@/misc/prelude/array.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
|
||||
@@ -33,6 +34,8 @@ export class MfmService {
|
||||
// some AP servers like Pixelfed use br tags as well as newlines
|
||||
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
|
||||
|
||||
const normalizedHashtagNames = hashtagNames == null ? undefined : new Set<string>(hashtagNames.map(x => normalizeForSearch(x)));
|
||||
|
||||
const dom = parse5.parseFragment(html);
|
||||
|
||||
let text = '';
|
||||
@@ -85,7 +88,7 @@ export class MfmService {
|
||||
const href = node.attrs.find(x => x.name === 'href');
|
||||
|
||||
// ハッシュタグ
|
||||
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
||||
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
|
||||
text += txt;
|
||||
// メンション
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||
@@ -244,6 +247,8 @@ export class MfmService {
|
||||
|
||||
const doc = window.document;
|
||||
|
||||
const body = doc.createElement('p');
|
||||
|
||||
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
|
||||
if (children) {
|
||||
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
|
||||
@@ -454,8 +459,8 @@ export class MfmService {
|
||||
},
|
||||
};
|
||||
|
||||
appendChildren(nodes, doc.body);
|
||||
appendChildren(nodes, body);
|
||||
|
||||
return `<p>${doc.body.innerHTML}</p>`;
|
||||
return new XMLSerializer().serializeToString(body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { HashtagService } from '@/core/HashtagService.js';
|
||||
import { AntennaService } from '@/core/AntennaService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
@@ -59,6 +59,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
@@ -151,8 +152,6 @@ type Option = {
|
||||
export class NoteCreateService implements OnApplicationShutdown {
|
||||
#shutdownController = new AbortController();
|
||||
|
||||
public static ContainsProhibitedWordsError = class extends Error {};
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
@@ -205,7 +204,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private hashtagService: HashtagService,
|
||||
private antennaService: AntennaService,
|
||||
private webhookService: WebhookService,
|
||||
private webhookService: UserWebhookService,
|
||||
private featuredService: FeaturedService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
@@ -263,8 +262,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) {
|
||||
throw new NoteCreateService.ContainsProhibitedWordsError();
|
||||
const hasProhibitedWords = await this.checkProhibitedWordsContain({
|
||||
cw: data.cw,
|
||||
text: data.text,
|
||||
pollChoices: data.poll?.choices,
|
||||
}, meta.prohibitedWords);
|
||||
|
||||
if (hasProhibitedWords) {
|
||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||
}
|
||||
|
||||
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
|
||||
@@ -300,7 +305,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (data.renote && !this.isQuote(data)) {
|
||||
if (this.isRenote(data) && !this.isQuote(data)) {
|
||||
if (data.renote.userHost === null) {
|
||||
if (data.renote.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
|
||||
@@ -379,6 +384,10 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) {
|
||||
throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions');
|
||||
}
|
||||
|
||||
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
||||
|
||||
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
||||
@@ -463,6 +472,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
noteVisibility: insert.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
channelId: insert.channelId,
|
||||
});
|
||||
|
||||
await transactionalEntityManager.insert(MiPoll, poll);
|
||||
@@ -595,7 +605,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
this.webhookService.getActiveWebhooks().then(webhooks => {
|
||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'note', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'note', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
@@ -622,7 +632,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'reply', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'reply', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
@@ -631,7 +641,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
// If it is renote
|
||||
if (data.renote) {
|
||||
if (this.isRenote(data)) {
|
||||
const type = this.isQuote(data) ? 'quote' : 'renote';
|
||||
|
||||
// Notify
|
||||
@@ -645,7 +655,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'renote', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'renote', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
@@ -715,9 +725,20 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private isQuote(note: Option): note is Option & { renote: MiNote } {
|
||||
// sync with misc/is-quote.ts
|
||||
return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll);
|
||||
private isRenote(note: Option): note is Option & { renote: MiNote } {
|
||||
return note.renote != null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private isQuote(note: Option & { renote: MiNote }): note is Option & { renote: MiNote } & (
|
||||
{ text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
|
||||
) {
|
||||
// NOTE: SYNC WITH misc/is-quote.ts
|
||||
return note.text != null ||
|
||||
note.reply != null ||
|
||||
note.cw != null ||
|
||||
note.poll != null ||
|
||||
(note.files != null && note.files.length > 0);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -766,7 +787,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'mention', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'mention', {
|
||||
note: detailPackedNote,
|
||||
});
|
||||
}
|
||||
@@ -785,7 +806,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
|
||||
if (data.localOnly) return null;
|
||||
|
||||
const content = data.renote && !this.isQuote(data)
|
||||
const content = this.isRenote(data) && !this.isQuote(data)
|
||||
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
||||
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
|
||||
|
||||
@@ -817,7 +838,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
const mentions = extractMentions(tokens);
|
||||
let mentionedUsers = (await Promise.all(mentions.map(m =>
|
||||
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
|
||||
))).filter(x => x != null) as MiUser[];
|
||||
))).filter(x => x != null);
|
||||
|
||||
// Drop duplicate users
|
||||
mentionedUsers = mentionedUsers.filter((u, i, self) =>
|
||||
@@ -991,6 +1012,23 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
|
||||
if (prohibitedWords == null) {
|
||||
prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
|
||||
}
|
||||
|
||||
if (
|
||||
this.utilityService.isKeyWordIncluded(
|
||||
this.utilityService.concatNoteContentsForKeyWordCheck(content),
|
||||
prohibitedWords,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.#shutdownController.abort();
|
||||
|
||||
@@ -24,7 +24,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteDeleteService {
|
||||
@@ -79,7 +79,7 @@ export class NoteDeleteService {
|
||||
let renote: MiNote | null = null;
|
||||
|
||||
// if deleted note is renote
|
||||
if (isPureRenote(note)) {
|
||||
if (isRenote(note) && !isQuote(note)) {
|
||||
renote = await this.notesRepository.findOneBy({
|
||||
id: note.renoteId,
|
||||
});
|
||||
|
||||
@@ -88,46 +88,47 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
userId: MiUser['id'],
|
||||
notes: (MiNote | Packed<'Note'>)[],
|
||||
): Promise<void> {
|
||||
const readMentions: (MiNote | Packed<'Note'>)[] = [];
|
||||
const readSpecifiedNotes: (MiNote | Packed<'Note'>)[] = [];
|
||||
if (notes.length === 0) return;
|
||||
|
||||
const noteIds = new Set<MiNote['id']>();
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.mentions && note.mentions.includes(userId)) {
|
||||
readMentions.push(note);
|
||||
noteIds.add(note.id);
|
||||
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
||||
readSpecifiedNotes.push(note);
|
||||
noteIds.add(note.id);
|
||||
}
|
||||
}
|
||||
|
||||
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
|
||||
// Remove the record
|
||||
await this.noteUnreadsRepository.delete({
|
||||
userId: userId,
|
||||
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
|
||||
});
|
||||
if (noteIds.size === 0) return;
|
||||
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
// Remove the record
|
||||
await this.noteUnreadsRepository.delete({
|
||||
userId: userId,
|
||||
noteId: In(Array.from(noteIds)),
|
||||
});
|
||||
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
}).then(mentionsCount => {
|
||||
if (mentionsCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
}));
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
}).then(specifiedCount => {
|
||||
if (specifiedCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
}));
|
||||
}
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
}).then(mentionsCount => {
|
||||
if (mentionsCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
}));
|
||||
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
}).then(specifiedCount => {
|
||||
if (specifiedCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
@@ -122,6 +122,14 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
return null;
|
||||
}
|
||||
} else if (recieveConfig?.type === 'mutualFollow') {
|
||||
const [isFollowing, isFollower] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
|
||||
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
|
||||
]);
|
||||
if (!(isFollowing && isFollower)) {
|
||||
return null;
|
||||
}
|
||||
} else if (recieveConfig?.type === 'followingOrFollower') {
|
||||
const [isFollowing, isFollower] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
|
||||
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
|
||||
@@ -155,6 +163,8 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
|
||||
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
|
||||
|
||||
if (packed == null) return null;
|
||||
|
||||
// Publish notification event
|
||||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||
|
||||
@@ -204,6 +214,15 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
*/
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async flushAllNotifications(userId: MiUser['id']) {
|
||||
await Promise.all([
|
||||
this.redisClient.del(`notificationTimeline:${userId}`),
|
||||
this.redisClient.del(`latestReadNotification:${userId}`),
|
||||
]);
|
||||
this.globalEventService.publishMainStream(userId, 'notificationFlushed');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.#shutdownController.abort();
|
||||
|
||||
@@ -101,7 +101,7 @@ export class PushNotificationService implements OnApplicationShutdown {
|
||||
type,
|
||||
body: (type === 'notification' || type === 'unreadAntennaNote') ? truncateBody(type, body) : body,
|
||||
userId,
|
||||
dateTime: (new Date()).getTime(),
|
||||
dateTime: Date.now(),
|
||||
}), {
|
||||
proxy: this.config.proxy,
|
||||
}).catch((err: any) => {
|
||||
@@ -115,12 +115,19 @@ export class PushNotificationService implements OnApplicationShutdown {
|
||||
endpoint: subscription.endpoint,
|
||||
auth: subscription.auth,
|
||||
publickey: subscription.publickey,
|
||||
}).then(() => {
|
||||
this.refreshCache(userId);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public refreshCache(userId: string): void {
|
||||
this.subscriptionsCache.refresh(userId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.subscriptionsCache.dispose();
|
||||
|
||||
@@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||
import { baseQueueOptions, QUEUE } from '@/queue/const.js';
|
||||
import { allSettled } from '@/misc/promise-tracker.js';
|
||||
import {
|
||||
DeliverJobData,
|
||||
EndedPollNotificationJobData,
|
||||
InboxJobData,
|
||||
RelationshipJobData,
|
||||
UserWebhookDeliverJobData,
|
||||
SystemWebhookDeliverJobData,
|
||||
} from '../queue/types.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
||||
|
||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||
@@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||
export type DbQueue = Bull.Queue;
|
||||
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
|
||||
export type ObjectStorageQueue = Bull.Queue;
|
||||
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
|
||||
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
|
||||
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
|
||||
|
||||
const $system: Provider = {
|
||||
provide: 'queue:system',
|
||||
@@ -63,9 +71,15 @@ const $objectStorage: Provider = {
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $webhookDeliver: Provider = {
|
||||
provide: 'queue:webhookDeliver',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
|
||||
const $userWebhookDeliver: Provider = {
|
||||
provide: 'queue:userWebhookDeliver',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $systemWebhookDeliver: Provider = {
|
||||
provide: 'queue:systemWebhookDeliver',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
@@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
|
||||
$db,
|
||||
$relationship,
|
||||
$objectStorage,
|
||||
$webhookDeliver,
|
||||
$userWebhookDeliver,
|
||||
$systemWebhookDeliver,
|
||||
],
|
||||
exports: [
|
||||
$system,
|
||||
@@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
|
||||
$db,
|
||||
$relationship,
|
||||
$objectStorage,
|
||||
$webhookDeliver,
|
||||
$userWebhookDeliver,
|
||||
$systemWebhookDeliver,
|
||||
],
|
||||
})
|
||||
export class QueueModule implements OnApplicationShutdown {
|
||||
@@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||
) {}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
@@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
|
||||
this.dbQueue.close(),
|
||||
this.relationshipQueue.close(),
|
||||
this.objectStorageQueue.close(),
|
||||
this.webhookDeliverQueue.close(),
|
||||
this.userWebhookDeliverQueue.close(),
|
||||
this.systemWebhookDeliverQueue.close(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,33 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
||||
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||
import type {
|
||||
DbJobData,
|
||||
DeliverJobData,
|
||||
RelationshipJobData,
|
||||
SystemWebhookDeliverJobData,
|
||||
ThinUser,
|
||||
UserWebhookDeliverJobData,
|
||||
} from '../queue/types.js';
|
||||
import type {
|
||||
DbQueue,
|
||||
DeliverQueue,
|
||||
EndedPollNotificationQueue,
|
||||
InboxQueue,
|
||||
ObjectStorageQueue,
|
||||
RelationshipQueue,
|
||||
SystemQueue,
|
||||
UserWebhookDeliverQueue,
|
||||
SystemWebhookDeliverQueue,
|
||||
} from './QueueModule.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
@@ -31,7 +49,8 @@ export class QueueService {
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
|
||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
|
||||
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||
) {
|
||||
this.systemQueue.add('tickCharts', {
|
||||
}, {
|
||||
@@ -431,9 +450,13 @@ export class QueueService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||
const data = {
|
||||
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||
const data: UserWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
webhookId: webhook.id,
|
||||
@@ -444,7 +467,33 @@ export class QueueService {
|
||||
eventId: randomUUID(),
|
||||
};
|
||||
|
||||
return this.webhookDeliverQueue.add(webhook.id, data, {
|
||||
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SystemWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
|
||||
const data: SystemWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
webhookId: webhook.id,
|
||||
to: webhook.url,
|
||||
secret: webhook.secret,
|
||||
createdAt: Date.now(),
|
||||
eventId: randomUUID(),
|
||||
};
|
||||
|
||||
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
|
||||
@@ -29,13 +29,14 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
const FALLBACK = '\u2764';
|
||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||
|
||||
const legacies: Record<string, string> = {
|
||||
'like': '👍',
|
||||
'love': '❤', // ここに記述する場合は異体字セレクタを入れない
|
||||
'love': '\u2764', // ハート、異体字セレクタを入れない
|
||||
'laugh': '😆',
|
||||
'hmm': '🤔',
|
||||
'surprise': '😮',
|
||||
@@ -117,11 +118,16 @@ export class ReactionService {
|
||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
// Check if note is Renote
|
||||
if (isRenote(note) && !isQuote(note)) {
|
||||
throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
|
||||
}
|
||||
|
||||
let reaction = _reaction ?? FALLBACK;
|
||||
|
||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
||||
reaction = '❤️';
|
||||
} else if (_reaction) {
|
||||
reaction = '\u2764';
|
||||
} else if (_reaction != null) {
|
||||
const custom = reaction.match(isCustomEmojiRegexp);
|
||||
if (custom) {
|
||||
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
||||
@@ -322,35 +328,36 @@ export class ReactionService {
|
||||
//#endregion
|
||||
}
|
||||
|
||||
/**
|
||||
* 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、
|
||||
* データベース上には存在する「0個のリアクションがついている」という情報を削除する。
|
||||
*/
|
||||
@bindThis
|
||||
public convertLegacyReactions(reactions: Record<string, number>) {
|
||||
const _reactions = {} as Record<string, number>;
|
||||
public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
|
||||
return Object.entries(reactions)
|
||||
.filter(([, count]) => {
|
||||
// `ReactionService.prototype.delete`ではリアクション削除時に、
|
||||
// `MiNote['reactions']`のエントリの値をデクリメントしているが、
|
||||
// デクリメントしているだけなのでエントリ自体は0を値として持つ形で残り続ける。
|
||||
// そのため、この処理がなければ、「0個のリアクションがついている」ということになってしまう。
|
||||
return count > 0;
|
||||
})
|
||||
.map(([reaction, count]) => {
|
||||
// unchecked indexed access
|
||||
const convertedReaction = legacies[reaction] as string | undefined;
|
||||
|
||||
for (const reaction of Object.keys(reactions)) {
|
||||
if (reactions[reaction] <= 0) continue;
|
||||
const key = this.decodeReaction(convertedReaction ?? reaction).reaction;
|
||||
|
||||
if (Object.keys(legacies).includes(reaction)) {
|
||||
if (_reactions[legacies[reaction]]) {
|
||||
_reactions[legacies[reaction]] += reactions[reaction];
|
||||
} else {
|
||||
_reactions[legacies[reaction]] = reactions[reaction];
|
||||
}
|
||||
} else {
|
||||
if (_reactions[reaction]) {
|
||||
_reactions[reaction] += reactions[reaction];
|
||||
} else {
|
||||
_reactions[reaction] = reactions[reaction];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [key, count] as const;
|
||||
})
|
||||
.reduce<MiNote['reactions']>((acc, [key, count]) => {
|
||||
// unchecked indexed access
|
||||
const prevCount = acc[key] as number | undefined;
|
||||
|
||||
const _reactions2 = {} as Record<string, number>;
|
||||
acc[key] = (prevCount ?? 0) + count;
|
||||
|
||||
for (const reaction of Object.keys(_reactions)) {
|
||||
_reactions2[this.decodeReaction(reaction).reaction] = _reactions[reaction];
|
||||
}
|
||||
|
||||
return _reactions2;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
@@ -53,11 +53,11 @@ export class RelayService {
|
||||
|
||||
@bindThis
|
||||
public async addRelay(inbox: string): Promise<MiRelay> {
|
||||
const relay = await this.relaysRepository.insert({
|
||||
const relay = await this.relaysRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
inbox,
|
||||
status: 'requesting',
|
||||
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
const relayActor = await this.getRelayActor();
|
||||
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
|
||||
@@ -281,7 +281,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
|
||||
@bindThis
|
||||
private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
const game = await this.reversiGamesRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
user1Id: parentId,
|
||||
user2Id: childId,
|
||||
@@ -294,10 +294,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
noIrregularRules: options.noIrregularRules,
|
||||
}).then(x => this.reversiGamesRepository.findOneOrFail({
|
||||
where: { id: x.identifiers[0].id },
|
||||
relations: ['user1', 'user2'],
|
||||
}));
|
||||
}, { relations: ['user1', 'user2'] });
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game);
|
||||
|
||||
@@ -35,6 +35,7 @@ export type RolePolicies = {
|
||||
gtlAvailable: boolean;
|
||||
ltlAvailable: boolean;
|
||||
canPublicNote: boolean;
|
||||
mentionLimit: number;
|
||||
canInvite: boolean;
|
||||
inviteLimit: number;
|
||||
inviteLimitCycle: number;
|
||||
@@ -62,6 +63,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
gtlAvailable: true,
|
||||
ltlAvailable: true,
|
||||
canPublicNote: true,
|
||||
mentionLimit: 20,
|
||||
canInvite: false,
|
||||
inviteLimit: 0,
|
||||
inviteLimitCycle: 60 * 24 * 7,
|
||||
@@ -200,45 +202,82 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private evalCond(user: MiUser, value: RoleCondFormulaValue): boolean {
|
||||
private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
|
||||
try {
|
||||
switch (value.type) {
|
||||
// ~かつ~
|
||||
case 'and': {
|
||||
return value.values.every(v => this.evalCond(user, v));
|
||||
return value.values.every(v => this.evalCond(user, roles, v));
|
||||
}
|
||||
// ~または~
|
||||
case 'or': {
|
||||
return value.values.some(v => this.evalCond(user, v));
|
||||
return value.values.some(v => this.evalCond(user, roles, v));
|
||||
}
|
||||
// ~ではない
|
||||
case 'not': {
|
||||
return !this.evalCond(user, value.value);
|
||||
return !this.evalCond(user, roles, value.value);
|
||||
}
|
||||
// マニュアルロールがアサインされている
|
||||
case 'roleAssignedTo': {
|
||||
return roles.some(r => r.id === value.roleId);
|
||||
}
|
||||
// ローカルユーザのみ
|
||||
case 'isLocal': {
|
||||
return this.userEntityService.isLocalUser(user);
|
||||
}
|
||||
// リモートユーザのみ
|
||||
case 'isRemote': {
|
||||
return this.userEntityService.isRemoteUser(user);
|
||||
}
|
||||
// サスペンド済みユーザである
|
||||
case 'isSuspended': {
|
||||
return user.isSuspended;
|
||||
}
|
||||
// 鍵アカウントユーザである
|
||||
case 'isLocked': {
|
||||
return user.isLocked;
|
||||
}
|
||||
// botユーザである
|
||||
case 'isBot': {
|
||||
return user.isBot;
|
||||
}
|
||||
// 猫である
|
||||
case 'isCat': {
|
||||
return user.isCat;
|
||||
}
|
||||
// 「ユーザを見つけやすくする」が有効なアカウント
|
||||
case 'isExplorable': {
|
||||
return user.isExplorable;
|
||||
}
|
||||
// ユーザが作成されてから指定期間経過した
|
||||
case 'createdLessThan': {
|
||||
return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000));
|
||||
}
|
||||
// ユーザが作成されてから指定期間経っていない
|
||||
case 'createdMoreThan': {
|
||||
return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000));
|
||||
}
|
||||
// フォロワー数が指定値以下
|
||||
case 'followersLessThanOrEq': {
|
||||
return user.followersCount <= value.value;
|
||||
}
|
||||
// フォロワー数が指定値以上
|
||||
case 'followersMoreThanOrEq': {
|
||||
return user.followersCount >= value.value;
|
||||
}
|
||||
// フォロー数が指定値以下
|
||||
case 'followingLessThanOrEq': {
|
||||
return user.followingCount <= value.value;
|
||||
}
|
||||
// フォロー数が指定値以上
|
||||
case 'followingMoreThanOrEq': {
|
||||
return user.followingCount >= value.value;
|
||||
}
|
||||
// ノート数が指定値以下
|
||||
case 'notesLessThanOrEq': {
|
||||
return user.notesCount <= value.value;
|
||||
}
|
||||
// ノート数が指定値以上
|
||||
case 'notesMoreThanOrEq': {
|
||||
return user.notesCount >= value.value;
|
||||
}
|
||||
@@ -272,7 +311,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
const assigns = await this.getUserAssigns(userId);
|
||||
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
|
||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
|
||||
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula));
|
||||
return [...assignedRoles, ...matchedCondRoles];
|
||||
}
|
||||
|
||||
@@ -285,13 +324,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
||||
// 期限切れのロールを除外
|
||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
||||
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
|
||||
const assignedBadgeRoles = assignedRoles.filter(r => r.asBadge);
|
||||
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
||||
if (badgeCondRoles.length > 0) {
|
||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
|
||||
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula));
|
||||
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
||||
} else {
|
||||
return assignedBadgeRoles;
|
||||
@@ -325,6 +364,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||
mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
|
||||
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
||||
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
|
||||
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
||||
@@ -370,14 +410,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> {
|
||||
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
|
||||
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||
roleId: In(moderatorRoles.map(r => r.id)),
|
||||
}) : [];
|
||||
const moderatorRoles = includeAdmins
|
||||
? roles.filter(r => r.isModerator || r.isAdministrator)
|
||||
: roles.filter(r => r.isModerator);
|
||||
|
||||
// TODO: isRootなアカウントも含める
|
||||
return assigns.map(a => a.userId);
|
||||
const assigns = moderatorRoles.length > 0
|
||||
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
|
||||
: [];
|
||||
|
||||
const now = Date.now();
|
||||
const result = [
|
||||
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
|
||||
...new Set(
|
||||
assigns
|
||||
.filter(it =>
|
||||
(excludeExpire)
|
||||
? (it.expiresAt == null || it.expiresAt.getTime() > now)
|
||||
: true,
|
||||
)
|
||||
.map(a => a.userId),
|
||||
),
|
||||
];
|
||||
|
||||
return result.sort((x, y) => x.localeCompare(y));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -431,12 +489,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
const created = await this.roleAssignmentsRepository.insert({
|
||||
const created = await this.roleAssignmentsRepository.insertOne({
|
||||
id: this.idService.gen(now),
|
||||
expiresAt: expiresAt,
|
||||
roleId: roleId,
|
||||
userId: userId,
|
||||
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
this.rolesRepository.update(roleId, {
|
||||
lastUsedAt: new Date(),
|
||||
@@ -518,7 +576,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
@bindThis
|
||||
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
|
||||
const date = new Date();
|
||||
const created = await this.rolesRepository.insert({
|
||||
const created = await this.rolesRepository.insertOne({
|
||||
id: this.idService.gen(date.getTime()),
|
||||
updatedAt: date,
|
||||
lastUsedAt: date,
|
||||
@@ -536,7 +594,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
canEditMembersByModerator: values.canEditMembersByModerator,
|
||||
displayOrder: values.displayOrder,
|
||||
policies: values.policies,
|
||||
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('roleCreated', created);
|
||||
|
||||
|
||||
233
packages/backend/src/core/SystemWebhookService.ts
Normal file
233
packages/backend/src/core/SystemWebhookService.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import Logger from '@/logger.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class SystemWebhookService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
private activeSystemWebhooksFetched = false;
|
||||
private activeSystemWebhooks: MiSystemWebhook[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
@Inject(DI.systemWebhooksRepository)
|
||||
private systemWebhooksRepository: SystemWebhooksRepository,
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private loggerService: LoggerService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
this.logger = this.loggerService.getLogger('webhook');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetchActiveSystemWebhooks() {
|
||||
if (!this.activeSystemWebhooksFetched) {
|
||||
this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
|
||||
isActive: true,
|
||||
});
|
||||
this.activeSystemWebhooksFetched = true;
|
||||
}
|
||||
|
||||
return this.activeSystemWebhooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook の一覧を取得する.
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchSystemWebhooks(params?: {
|
||||
ids?: MiSystemWebhook['id'][];
|
||||
isActive?: MiSystemWebhook['isActive'];
|
||||
on?: MiSystemWebhook['on'];
|
||||
}): Promise<MiSystemWebhook[]> {
|
||||
const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
|
||||
if (params) {
|
||||
if (params.ids && params.ids.length > 0) {
|
||||
query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
|
||||
}
|
||||
if (params.isActive !== undefined) {
|
||||
query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
|
||||
}
|
||||
if (params.on && params.on.length > 0) {
|
||||
query.andWhere(':on <@ systemWebhook.on', { on: params.on });
|
||||
}
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook を作成する.
|
||||
*/
|
||||
@bindThis
|
||||
public async createSystemWebhook(
|
||||
params: {
|
||||
isActive: MiSystemWebhook['isActive'];
|
||||
name: MiSystemWebhook['name'];
|
||||
on: MiSystemWebhook['on'];
|
||||
url: MiSystemWebhook['url'];
|
||||
secret: MiSystemWebhook['secret'];
|
||||
},
|
||||
updater: MiUser,
|
||||
): Promise<MiSystemWebhook> {
|
||||
const id = this.idService.gen();
|
||||
await this.systemWebhooksRepository.insert({
|
||||
...params,
|
||||
id,
|
||||
});
|
||||
|
||||
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
|
||||
this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
|
||||
this.moderationLogService
|
||||
.log(updater, 'createSystemWebhook', {
|
||||
systemWebhookId: webhook.id,
|
||||
webhook: webhook,
|
||||
})
|
||||
.then();
|
||||
|
||||
return webhook;
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook を更新する.
|
||||
*/
|
||||
@bindThis
|
||||
public async updateSystemWebhook(
|
||||
params: {
|
||||
id: MiSystemWebhook['id'];
|
||||
isActive: MiSystemWebhook['isActive'];
|
||||
name: MiSystemWebhook['name'];
|
||||
on: MiSystemWebhook['on'];
|
||||
url: MiSystemWebhook['url'];
|
||||
secret: MiSystemWebhook['secret'];
|
||||
},
|
||||
updater: MiUser,
|
||||
): Promise<MiSystemWebhook> {
|
||||
const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
|
||||
await this.systemWebhooksRepository.update(beforeEntity.id, {
|
||||
updatedAt: new Date(),
|
||||
isActive: params.isActive,
|
||||
name: params.name,
|
||||
on: params.on,
|
||||
url: params.url,
|
||||
secret: params.secret,
|
||||
});
|
||||
|
||||
const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
|
||||
this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
|
||||
this.moderationLogService
|
||||
.log(updater, 'updateSystemWebhook', {
|
||||
systemWebhookId: beforeEntity.id,
|
||||
before: beforeEntity,
|
||||
after: afterEntity,
|
||||
})
|
||||
.then();
|
||||
|
||||
return afterEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook を削除する.
|
||||
*/
|
||||
@bindThis
|
||||
public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
|
||||
const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
|
||||
await this.systemWebhooksRepository.delete(id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
|
||||
this.moderationLogService
|
||||
.log(updater, 'deleteSystemWebhook', {
|
||||
systemWebhookId: webhook.id,
|
||||
webhook,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook をWebhook配送キューに追加する
|
||||
* @see QueueService.systemWebhookDeliver
|
||||
*/
|
||||
@bindThis
|
||||
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
|
||||
const webhookEntity = typeof webhook === 'string'
|
||||
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||
: webhook;
|
||||
if (!webhookEntity || !webhookEntity.isActive) {
|
||||
this.logger.info(`Webhook is not active or not found : ${webhook}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookEntity.on.includes(type)) {
|
||||
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
if (obj.channel !== 'internal') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'systemWebhookCreated': {
|
||||
if (body.isActive) {
|
||||
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'systemWebhookUpdated': {
|
||||
if (body.isActive) {
|
||||
const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
|
||||
} else {
|
||||
this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
|
||||
}
|
||||
} else {
|
||||
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'systemWebhookDeleted': {
|
||||
this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import Logger from '@/logger.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
@@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private webhookService: WebhookService,
|
||||
private webhookService: UserWebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
@@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
@@ -30,6 +30,7 @@ import type { Config } from '@/config.js';
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import type { ThinUser } from '@/queue/types.js';
|
||||
import Logger from '../logger.js';
|
||||
|
||||
const logger = new Logger('following/create');
|
||||
@@ -81,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
private metaService: MetaService,
|
||||
private notificationService: NotificationService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private webhookService: WebhookService,
|
||||
private webhookService: UserWebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
@@ -94,21 +95,35 @@ export class UserFollowingService implements OnModuleInit {
|
||||
this.userBlockingService = this.moduleRef.get('UserBlockingService');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deliverAccept(follower: MiRemoteUser, followee: MiPartialLocalUser, requestId?: string) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async follow(
|
||||
_follower: { id: MiUser['id'] },
|
||||
_followee: { id: MiUser['id'] },
|
||||
_follower: ThinUser,
|
||||
_followee: ThinUser,
|
||||
{ requestId, silent = false, withReplies }: {
|
||||
requestId?: string,
|
||||
silent?: boolean,
|
||||
withReplies?: boolean,
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
/**
|
||||
* 必ず最新のユーザー情報を取得する
|
||||
*/
|
||||
const [follower, followee] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: _follower.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: _followee.id }),
|
||||
]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser];
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
// What?
|
||||
throw new Error('Remote user cannot follow remote user.');
|
||||
}
|
||||
|
||||
// check blocking
|
||||
const [blocking, blocked] = await Promise.all([
|
||||
this.userBlockingService.checkBlocked(follower.id, followee.id),
|
||||
@@ -129,6 +144,24 @@ export class UserFollowingService implements OnModuleInit {
|
||||
if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
|
||||
}
|
||||
|
||||
if (await this.followingsRepository.exists({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
})) {
|
||||
// すでにフォロー関係が存在している場合
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
// リモート → ローカル: acceptを送り返しておしまい
|
||||
this.deliverAccept(follower, followee, requestId);
|
||||
return;
|
||||
}
|
||||
if (this.userEntityService.isLocalUser(follower)) {
|
||||
// ローカル → リモート/ローカル: 例外
|
||||
throw new IdentifiableError('ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced', 'already following');
|
||||
}
|
||||
}
|
||||
|
||||
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
|
||||
// フォロー対象が鍵アカウントである or
|
||||
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
||||
@@ -189,8 +222,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
await this.insertFollowingDoc(followee, follower, silent, withReplies);
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||
this.deliverAccept(follower, followee, requestId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +331,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'follow', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'follow', {
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
@@ -313,7 +345,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'followed', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'followed', {
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
@@ -366,7 +398,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
@@ -479,7 +511,13 @@ export class UserFollowingService implements OnModuleInit {
|
||||
if (blocking) throw new Error('blocking');
|
||||
if (blocked) throw new Error('blocked');
|
||||
|
||||
const followRequest = await this.followRequestsRepository.insert({
|
||||
// Remove old follow requests before creating a new one.
|
||||
await this.followRequestsRepository.delete({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
});
|
||||
|
||||
const followRequest = await this.followRequestsRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
@@ -493,7 +531,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
followeeHost: followee.host,
|
||||
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
|
||||
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
|
||||
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
|
||||
// Publish receiveRequest event
|
||||
if (this.userEntityService.isLocalUser(followee)) {
|
||||
@@ -571,8 +609,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
await this.insertFollowingDoc(followee, follower, false, request.withReplies);
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||
this.deliverAccept(follower, followee as MiPartialLocalUser, request.requestId ?? undefined);
|
||||
}
|
||||
|
||||
this.userEntityService.pack(followee.id, followee, {
|
||||
@@ -703,7 +740,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||||
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
|
||||
user: packedFollowee,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
|
||||
const currentCount = await this.userListMembershipsRepository.countBy({
|
||||
userListId: list.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||
throw new UserListService.TooManyUsersError();
|
||||
}
|
||||
|
||||
|
||||
99
packages/backend/src/core/UserWebhookService.ts
Normal file
99
packages/backend/src/core/UserWebhookService.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { WebhooksRepository } from '@/models/_.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class UserWebhookService implements OnApplicationShutdown {
|
||||
private activeWebhooksFetched = false;
|
||||
private activeWebhooks: MiWebhook[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
@Inject(DI.webhooksRepository)
|
||||
private webhooksRepository: WebhooksRepository,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveWebhooks() {
|
||||
if (!this.activeWebhooksFetched) {
|
||||
this.activeWebhooks = await this.webhooksRepository.findBy({
|
||||
active: true,
|
||||
});
|
||||
this.activeWebhooksFetched = true;
|
||||
}
|
||||
|
||||
return this.activeWebhooks;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
if (obj.channel !== 'internal') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'webhookCreated': {
|
||||
if (body.active) {
|
||||
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'webhookUpdated': {
|
||||
if (body.active) {
|
||||
const i = this.activeWebhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
} else {
|
||||
this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'webhookDeleted': {
|
||||
this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,20 @@ export class UtilityService {
|
||||
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public concatNoteContentsForKeyWordCheck(content: {
|
||||
cw?: string | null;
|
||||
text?: string | null;
|
||||
pollChoices?: string[] | null;
|
||||
others?: string[] | null;
|
||||
}): string {
|
||||
/**
|
||||
* ノートの内容を結合してキーワードチェック用の文字列を生成する
|
||||
* cwとtextは内容が繋がっているかもしれないので間に何も入れずにチェックする
|
||||
*/
|
||||
return `${content.cw ?? ''}${content.text ?? ''}\n${(content.pollChoices ?? []).join('\n')}\n${(content.others ?? []).join('\n')}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
|
||||
if (keyWords.length === 0) return false;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
generateRegistrationOptions, verifyAuthenticationResponse,
|
||||
verifyRegistrationResponse,
|
||||
} from '@simplewebauthn/server';
|
||||
import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers';
|
||||
import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UserSecurityKeysRepository } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -26,7 +26,7 @@ import type {
|
||||
PublicKeyCredentialDescriptorFuture,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/typescript-types';
|
||||
} from '@simplewebauthn/types';
|
||||
|
||||
@Injectable()
|
||||
export class WebAuthnService {
|
||||
@@ -49,7 +49,7 @@ export class WebAuthnService {
|
||||
const instance = await this.metaService.fetch();
|
||||
return {
|
||||
origin: this.config.url,
|
||||
rpId: this.config.host,
|
||||
rpId: this.config.hostname,
|
||||
rpName: instance.name ?? this.config.host,
|
||||
rpIcon: instance.iconUrl ?? undefined,
|
||||
};
|
||||
@@ -65,13 +65,12 @@ export class WebAuthnService {
|
||||
const registrationOptions = await generateRegistrationOptions({
|
||||
rpName: relyingParty.rpName,
|
||||
rpID: relyingParty.rpId,
|
||||
userID: userId,
|
||||
userID: isoUint8Array.fromUTF8String(userId),
|
||||
userName: userName,
|
||||
userDisplayName: userDisplayName,
|
||||
attestationType: 'indirect',
|
||||
excludeCredentials: keys.map(key => (<PublicKeyCredentialDescriptorFuture>{
|
||||
id: Buffer.from(key.id, 'base64url'),
|
||||
type: 'public-key',
|
||||
excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
|
||||
id: key.id,
|
||||
transports: key.transports ?? undefined,
|
||||
})),
|
||||
authenticatorSelection: {
|
||||
@@ -87,7 +86,7 @@ export class WebAuthnService {
|
||||
|
||||
@bindThis
|
||||
public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{
|
||||
credentialID: Uint8Array;
|
||||
credentialID: string;
|
||||
credentialPublicKey: Uint8Array;
|
||||
attestationObject: Uint8Array;
|
||||
fmt: AttestationFormat;
|
||||
@@ -144,6 +143,7 @@ export class WebAuthnService {
|
||||
|
||||
@bindThis
|
||||
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
const keys = await this.userSecurityKeysRepository.findBy({
|
||||
userId: userId,
|
||||
});
|
||||
@@ -153,9 +153,9 @@ export class WebAuthnService {
|
||||
}
|
||||
|
||||
const authenticationOptions = await generateAuthenticationOptions({
|
||||
allowCredentials: keys.map(key => (<PublicKeyCredentialDescriptorFuture>{
|
||||
id: Buffer.from(key.id, 'base64url'),
|
||||
type: 'public-key',
|
||||
rpID: relyingParty.rpId,
|
||||
allowCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
|
||||
id: key.id,
|
||||
transports: key.transports ?? undefined,
|
||||
})),
|
||||
userVerification: 'preferred',
|
||||
@@ -191,7 +191,7 @@ export class WebAuthnService {
|
||||
if (cert[0] === 0x04) { // 前の実装ではいつも 0x04 で始まっていた
|
||||
const halfLength = (cert.length - 1) / 2;
|
||||
|
||||
const cborMap = new Map<number, number | ArrayBufferLike>();
|
||||
const cborMap = new Map<number, number | Uint8Array>();
|
||||
cborMap.set(1, 2); // kty, EC2
|
||||
cborMap.set(3, -7); // alg, ES256
|
||||
cborMap.set(-1, 1); // crv, P256
|
||||
@@ -219,7 +219,7 @@ export class WebAuthnService {
|
||||
expectedOrigin: relyingParty.origin,
|
||||
expectedRPID: relyingParty.rpId,
|
||||
authenticator: {
|
||||
credentialID: Buffer.from(key.id, 'base64url'),
|
||||
credentialID: key.id,
|
||||
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
|
||||
counter: key.counter,
|
||||
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { WebhooksRepository } from '@/models/_.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class WebhookService implements OnApplicationShutdown {
|
||||
private webhooksFetched = false;
|
||||
private webhooks: MiWebhook[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.webhooksRepository)
|
||||
private webhooksRepository: WebhooksRepository,
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveWebhooks() {
|
||||
if (!this.webhooksFetched) {
|
||||
this.webhooks = await this.webhooksRepository.findBy({
|
||||
active: true,
|
||||
});
|
||||
this.webhooksFetched = true;
|
||||
}
|
||||
|
||||
return this.webhooks;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'webhookCreated':
|
||||
if (body.active) {
|
||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'webhookUpdated':
|
||||
if (body.active) {
|
||||
const i = this.webhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
} else {
|
||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
|
||||
}
|
||||
break;
|
||||
case 'webhookDeleted':
|
||||
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export class ApAudienceService {
|
||||
const limit = promiseLimit<MiUser | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
|
||||
)).filter((x): x is MiUser => x != null);
|
||||
)).filter(x => x != null);
|
||||
|
||||
if (toGroups.public.length > 0) {
|
||||
return {
|
||||
|
||||
@@ -106,12 +106,12 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||
|
||||
return await this.cacheService.userByIdCache.fetchMaybe(
|
||||
parsed.id,
|
||||
() => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined),
|
||||
() => this.usersRepository.findOneBy({ id: parsed.id, isDeleted: false }).then(x => x ?? undefined),
|
||||
) as MiLocalUser | undefined ?? null;
|
||||
} else {
|
||||
return await this.cacheService.uriPersonCache.fetch(
|
||||
parsed.uri,
|
||||
() => this.usersRepository.findOneBy({ uri: parsed.uri }),
|
||||
() => this.usersRepository.findOneBy({ uri: parsed.uri, isDeleted: false }),
|
||||
) as MiRemoteUser | null;
|
||||
}
|
||||
}
|
||||
@@ -136,8 +136,12 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||
|
||||
if (key == null) return null;
|
||||
|
||||
const user = await this.cacheService.findUserById(key.userId).catch(() => null) as MiRemoteUser | null;
|
||||
if (user == null) return null;
|
||||
if (user.isDeleted) return null;
|
||||
|
||||
return {
|
||||
user: await this.cacheService.findUserById(key.userId) as MiRemoteUser,
|
||||
user,
|
||||
key,
|
||||
};
|
||||
}
|
||||
@@ -151,6 +155,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||
key: MiUserPublickey | null;
|
||||
} | null> {
|
||||
const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser;
|
||||
if (user.isDeleted) return null;
|
||||
|
||||
const key = await this.publicKeyByUserIdCache.fetch(
|
||||
user.id,
|
||||
|
||||
@@ -27,6 +27,8 @@ import { QueueService } from '@/core/QueueService.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||
import { ApNoteService } from './models/ApNoteService.js';
|
||||
import { ApLoggerService } from './ApLoggerService.js';
|
||||
@@ -36,7 +38,7 @@ import { ApAudienceService } from './ApAudienceService.js';
|
||||
import { ApPersonService } from './models/ApPersonService.js';
|
||||
import { ApQuestionService } from './models/ApQuestionService.js';
|
||||
import type { Resolver } from './ApResolverService.js';
|
||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
|
||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApInboxService {
|
||||
@@ -55,9 +57,6 @@ export class ApInboxService {
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.abuseUserReportsRepository)
|
||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
@@ -66,6 +65,7 @@ export class ApInboxService {
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private abuseReportService: AbuseReportService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private apAudienceService: ApAudienceService,
|
||||
private reactionService: ReactionService,
|
||||
@@ -82,18 +82,21 @@ export class ApInboxService {
|
||||
private apPersonService: ApPersonService,
|
||||
private apQuestionService: ApQuestionService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
this.logger = this.apLoggerService.logger;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
|
||||
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
||||
let result = undefined as string | void;
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
const results = [] as [string, string | void][];
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||
const act = await resolver.resolve(item);
|
||||
try {
|
||||
await this.performOneActivity(actor, act);
|
||||
results.push([getApId(item), await this.performOneActivity(actor, act)]);
|
||||
} catch (err) {
|
||||
if (err instanceof Error || typeof err === 'string') {
|
||||
this.logger.error(err);
|
||||
@@ -102,8 +105,13 @@ export class ApInboxService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok')));
|
||||
if (hasReason) {
|
||||
result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
|
||||
}
|
||||
} else {
|
||||
await this.performOneActivity(actor, activity);
|
||||
result = await this.performOneActivity(actor, activity);
|
||||
}
|
||||
|
||||
// ついでにリモートユーザーの情報が古かったら更新しておく
|
||||
@@ -114,42 +122,43 @@ export class ApInboxService {
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<void> {
|
||||
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
||||
if (actor.isSuspended) return;
|
||||
|
||||
if (isCreate(activity)) {
|
||||
await this.create(actor, activity);
|
||||
return await this.create(actor, activity);
|
||||
} else if (isDelete(activity)) {
|
||||
await this.delete(actor, activity);
|
||||
return await this.delete(actor, activity);
|
||||
} else if (isUpdate(activity)) {
|
||||
await this.update(actor, activity);
|
||||
return await this.update(actor, activity);
|
||||
} else if (isFollow(activity)) {
|
||||
await this.follow(actor, activity);
|
||||
return await this.follow(actor, activity);
|
||||
} else if (isAccept(activity)) {
|
||||
await this.accept(actor, activity);
|
||||
return await this.accept(actor, activity);
|
||||
} else if (isReject(activity)) {
|
||||
await this.reject(actor, activity);
|
||||
return await this.reject(actor, activity);
|
||||
} else if (isAdd(activity)) {
|
||||
await this.add(actor, activity).catch(err => this.logger.error(err));
|
||||
return await this.add(actor, activity);
|
||||
} else if (isRemove(activity)) {
|
||||
await this.remove(actor, activity).catch(err => this.logger.error(err));
|
||||
return await this.remove(actor, activity);
|
||||
} else if (isAnnounce(activity)) {
|
||||
await this.announce(actor, activity);
|
||||
return await this.announce(actor, activity);
|
||||
} else if (isLike(activity)) {
|
||||
await this.like(actor, activity);
|
||||
return await this.like(actor, activity);
|
||||
} else if (isUndo(activity)) {
|
||||
await this.undo(actor, activity);
|
||||
return await this.undo(actor, activity);
|
||||
} else if (isBlock(activity)) {
|
||||
await this.block(actor, activity);
|
||||
return await this.block(actor, activity);
|
||||
} else if (isFlag(activity)) {
|
||||
await this.flag(actor, activity);
|
||||
return await this.flag(actor, activity);
|
||||
} else if (isMove(activity)) {
|
||||
await this.move(actor, activity);
|
||||
return await this.move(actor, activity);
|
||||
} else {
|
||||
this.logger.warn(`unrecognized activity type: ${activity.type}`);
|
||||
return `unrecognized activity type: ${activity.type}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,38 +240,49 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async add(actor: MiRemoteUser, activity: IAdd): Promise<void> {
|
||||
private async add(actor: MiRemoteUser, activity: IAdd): Promise<string | void> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
||||
if (activity.target == null) {
|
||||
throw new Error('target is null');
|
||||
return 'target is null';
|
||||
}
|
||||
|
||||
if (activity.target === actor.featured) {
|
||||
const note = await this.apNoteService.resolveNote(activity.object);
|
||||
if (note == null) throw new Error('note not found');
|
||||
if (note == null) return 'note not found';
|
||||
await this.notePiningService.addPinned(actor, note.id);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`unknown target: ${activity.target}`);
|
||||
return `unknown target: ${activity.target}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<void> {
|
||||
private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<string | void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
this.logger.info(`Announce: ${uri}`);
|
||||
|
||||
const targetUri = getApId(activity.object);
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
|
||||
await this.announceNote(actor, activity, targetUri);
|
||||
if (!activity.object) return 'skip: activity has no object property';
|
||||
const targetUri = getApId(activity.object);
|
||||
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
|
||||
|
||||
const target = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
return e;
|
||||
});
|
||||
|
||||
if (isPost(target)) return await this.announceNote(actor, activity, target);
|
||||
|
||||
return `skip: unknown object type ${getApType(target)}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
|
||||
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise<string | void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
if (actor.isSuspended) {
|
||||
@@ -285,24 +305,21 @@ export class ApInboxService {
|
||||
// Announce対象をresolve
|
||||
let renote;
|
||||
try {
|
||||
renote = await this.apNoteService.resolveNote(targetUri);
|
||||
if (renote == null) throw new Error('announce target is null');
|
||||
renote = await this.apNoteService.resolveNote(target);
|
||||
if (renote == null) return 'announce target is null';
|
||||
} catch (err) {
|
||||
// 対象が4xxならスキップ
|
||||
if (err instanceof StatusError) {
|
||||
if (!err.isRetryable) {
|
||||
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
|
||||
return;
|
||||
return `Ignored announce target ${target.id} - ${err.statusCode}`;
|
||||
}
|
||||
|
||||
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
|
||||
return `Error in announce target ${target.id} - ${err.statusCode}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
|
||||
this.logger.warn('skip: invalid actor for this activity');
|
||||
return;
|
||||
return 'skip: invalid actor for this activity';
|
||||
}
|
||||
|
||||
this.logger.info(`Creating the (Re)Note: ${uri}`);
|
||||
@@ -311,8 +328,7 @@ export class ApInboxService {
|
||||
const createdAt = activity.published ? new Date(activity.published) : null;
|
||||
|
||||
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
|
||||
this.logger.warn('skip: malformed createdAt');
|
||||
return;
|
||||
return 'skip: malformed createdAt';
|
||||
}
|
||||
|
||||
await this.noteCreateService.create(actor, {
|
||||
@@ -346,11 +362,15 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async create(actor: MiRemoteUser, activity: ICreate): Promise<void> {
|
||||
private async create(actor: MiRemoteUser, activity: ICreate): Promise<string | void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
this.logger.info(`Create: ${uri}`);
|
||||
|
||||
if (!activity.object) return 'skip: activity has no object property';
|
||||
const targetUri = getApId(activity.object);
|
||||
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
|
||||
|
||||
// copy audiences between activity <=> object.
|
||||
if (typeof activity.object === 'object') {
|
||||
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
|
||||
@@ -377,7 +397,7 @@ export class ApInboxService {
|
||||
if (isPost(object)) {
|
||||
await this.createNote(resolver, actor, object, false, activity);
|
||||
} else {
|
||||
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
||||
return `Unknown type: ${getApType(object)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +439,7 @@ export class ApInboxService {
|
||||
@bindThis
|
||||
private async delete(actor: MiRemoteUser, activity: IDelete): Promise<string> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
||||
// 削除対象objectのtype
|
||||
@@ -479,6 +499,8 @@ export class ApInboxService {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
|
||||
|
||||
return `ok: queued ${job.name} ${job.id}`;
|
||||
}
|
||||
|
||||
@@ -515,20 +537,19 @@ export class ApInboxService {
|
||||
const userIds = uris
|
||||
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
||||
.map(uri => uri.split('/').at(-1))
|
||||
.filter((userId): userId is string => userId !== undefined);
|
||||
.filter(x => x != null);
|
||||
const users = await this.usersRepository.findBy({
|
||||
id: In(userIds),
|
||||
});
|
||||
if (users.length < 1) return 'skip';
|
||||
|
||||
await this.abuseUserReportsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
await this.abuseReportService.report([{
|
||||
targetUserId: users[0].id,
|
||||
targetUserHost: users[0].host,
|
||||
reporterId: actor.id,
|
||||
reporterHost: actor.host,
|
||||
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
|
||||
});
|
||||
}]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
@@ -576,29 +597,29 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async remove(actor: MiRemoteUser, activity: IRemove): Promise<void> {
|
||||
private async remove(actor: MiRemoteUser, activity: IRemove): Promise<string | void> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
||||
if (activity.target == null) {
|
||||
throw new Error('target is null');
|
||||
return 'target is null';
|
||||
}
|
||||
|
||||
if (activity.target === actor.featured) {
|
||||
const note = await this.apNoteService.resolveNote(activity.object);
|
||||
if (note == null) throw new Error('note not found');
|
||||
if (note == null) return 'note not found';
|
||||
await this.notePiningService.removePinned(actor, note.id);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`unknown target: ${activity.target}`);
|
||||
return `unknown target: ${activity.target}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async undo(actor: MiRemoteUser, activity: IUndo): Promise<string> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
||||
const uri = activity.id ?? activity;
|
||||
@@ -609,7 +630,7 @@ export class ApInboxService {
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
throw e;
|
||||
return e;
|
||||
});
|
||||
|
||||
// don't queue because the sender may attempt again when timeout
|
||||
|
||||
@@ -26,10 +26,10 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { LdSignatureService } from './LdSignatureService.js';
|
||||
import { JsonLdService } from './JsonLdService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import { CONTEXT } from './misc/contexts.js';
|
||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -56,7 +56,7 @@ export class ApRendererService {
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private ldSignatureService: LdSignatureService,
|
||||
private jsonLdService: JsonLdService,
|
||||
private userKeypairService: UserKeypairService,
|
||||
private apMfmService: ApMfmService,
|
||||
private mfmService: MfmService,
|
||||
@@ -166,6 +166,7 @@ export class ApRendererService {
|
||||
mediaType: file.webpublicType ?? file.type,
|
||||
url: this.driveFileEntityService.getPublicUrl(file),
|
||||
name: file.comment,
|
||||
sensitive: file.isSensitive,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -315,7 +316,7 @@ export class ApRendererService {
|
||||
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||
return ids.map(id => items.find(item => item.id === id)).filter((item): item is MiDriveFile => item != null);
|
||||
return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
|
||||
};
|
||||
|
||||
let inReplyTo;
|
||||
@@ -617,48 +618,16 @@ export class ApRendererService {
|
||||
x.id = `${this.config.url}/${randomUUID()}`;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{
|
||||
Key: 'sec:Key',
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
Hashtag: 'as:Hashtag',
|
||||
quoteUrl: 'as:quoteUrl',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
value: 'schema:value',
|
||||
// Misskey
|
||||
misskey: 'https://misskey-hub.net/ns#',
|
||||
'_misskey_content': 'misskey:_misskey_content',
|
||||
'_misskey_quote': 'misskey:_misskey_quote',
|
||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_summary': 'misskey:_misskey_summary',
|
||||
'isCat': 'misskey:isCat',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
},
|
||||
],
|
||||
}, x as T & { id: string });
|
||||
return Object.assign({ '@context': CONTEXT }, x as T & { id: string });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> {
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
const ldSignature = this.ldSignatureService.use();
|
||||
ldSignature.debug = false;
|
||||
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
|
||||
const jsonLd = this.jsonLdService.use();
|
||||
jsonLd.debug = false;
|
||||
activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
|
||||
|
||||
return activity;
|
||||
}
|
||||
@@ -716,7 +685,7 @@ export class ApRendererService {
|
||||
if (names.length === 0) return [];
|
||||
|
||||
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
||||
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
||||
const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null);
|
||||
|
||||
return emojis;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
||||
|
||||
type Request = {
|
||||
url: string;
|
||||
@@ -70,7 +71,7 @@ export class ApRequestCreator {
|
||||
url: u.href,
|
||||
method: 'GET',
|
||||
headers: this.#objectAssignWithLcKey({
|
||||
'Accept': 'application/activity+json, application/ld+json',
|
||||
'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'Date': new Date().toUTCString(),
|
||||
'Host': new URL(args.url).host,
|
||||
}, args.additionalHeaders),
|
||||
@@ -195,6 +196,9 @@ export class ApRequestService {
|
||||
const res = await this.httpRequestService.send(url, {
|
||||
method: req.request.method,
|
||||
headers: req.request.headers,
|
||||
}, {
|
||||
throwErrorWhenResponseNotOk: true,
|
||||
validators: [validateContentTypeSetAsActivityPub],
|
||||
});
|
||||
|
||||
return await res.json();
|
||||
|
||||
@@ -105,7 +105,7 @@ export class Resolver {
|
||||
|
||||
const object = (this.user
|
||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
||||
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
||||
: await this.httpRequestService.getActivityJson(value)) as IObject;
|
||||
|
||||
if (
|
||||
Array.isArray(object['@context']) ?
|
||||
|
||||
@@ -7,13 +7,14 @@ import * as crypto from 'node:crypto';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CONTEXTS } from './misc/contexts.js';
|
||||
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
|
||||
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
|
||||
import type { JsonLdDocument } from 'jsonld';
|
||||
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
|
||||
import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.js';
|
||||
|
||||
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
|
||||
// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
|
||||
|
||||
class LdSignature {
|
||||
class JsonLd {
|
||||
public debug = false;
|
||||
public preLoad = true;
|
||||
public loderTimeout = 5000;
|
||||
@@ -88,10 +89,18 @@ class LdSignature {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async normalize(data: JsonLdDocument): Promise<string> {
|
||||
public async compact(data: any, context: any = CONTEXT): Promise<JsonLdDocument> {
|
||||
const customLoader = this.getLoader();
|
||||
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
||||
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
||||
return (await import('jsonld')).default.compact(data, context, {
|
||||
documentLoader: customLoader,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async normalize(data: JsonLdDocument): Promise<string> {
|
||||
const customLoader = this.getLoader();
|
||||
return (await import('jsonld')).default.normalize(data, {
|
||||
documentLoader: customLoader,
|
||||
});
|
||||
@@ -103,11 +112,11 @@ class LdSignature {
|
||||
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
|
||||
|
||||
if (this.preLoad) {
|
||||
if (url in CONTEXTS) {
|
||||
if (url in PRELOADED_CONTEXTS) {
|
||||
if (this.debug) console.debug(`HIT: ${url}`);
|
||||
return {
|
||||
contextUrl: undefined,
|
||||
document: CONTEXTS[url],
|
||||
document: PRELOADED_CONTEXTS[url],
|
||||
documentUrl: url,
|
||||
};
|
||||
}
|
||||
@@ -124,7 +133,7 @@ class LdSignature {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchDocument(url: string): Promise<JsonLd> {
|
||||
private async fetchDocument(url: string): Promise<JsonLdObject> {
|
||||
const json = await this.httpRequestService.send(
|
||||
url,
|
||||
{
|
||||
@@ -133,7 +142,10 @@ class LdSignature {
|
||||
},
|
||||
timeout: this.loderTimeout,
|
||||
},
|
||||
{ throwErrorWhenResponseNotOk: false },
|
||||
{
|
||||
throwErrorWhenResponseNotOk: false,
|
||||
validators: [validateContentTypeSetAsJsonLD],
|
||||
},
|
||||
).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`${res.status} ${res.statusText}`);
|
||||
@@ -142,7 +154,7 @@ class LdSignature {
|
||||
}
|
||||
});
|
||||
|
||||
return json as JsonLd;
|
||||
return json as JsonLdObject;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -154,14 +166,14 @@ class LdSignature {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LdSignatureService {
|
||||
export class JsonLdService {
|
||||
constructor(
|
||||
private httpRequestService: HttpRequestService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public use(): LdSignature {
|
||||
return new LdSignature(this.httpRequestService);
|
||||
public use(): JsonLd {
|
||||
return new JsonLd(this.httpRequestService);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { JsonLd } from 'jsonld/jsonld-spec.js';
|
||||
import type { Context, JsonLd } from 'jsonld/jsonld-spec.js';
|
||||
|
||||
/* eslint:disable:quotemark indent */
|
||||
const id_v1 = {
|
||||
@@ -526,7 +526,42 @@ const activitystreams = {
|
||||
},
|
||||
} satisfies JsonLd;
|
||||
|
||||
export const CONTEXTS: Record<string, JsonLd> = {
|
||||
const context_iris = [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
];
|
||||
|
||||
const extension_context_definition = {
|
||||
Key: 'sec:Key',
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
Hashtag: 'as:Hashtag',
|
||||
quoteUrl: 'as:quoteUrl',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
value: 'schema:value',
|
||||
// Misskey
|
||||
misskey: 'https://misskey-hub.net/ns#',
|
||||
'_misskey_content': 'misskey:_misskey_content',
|
||||
'_misskey_quote': 'misskey:_misskey_quote',
|
||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_summary': 'misskey:_misskey_summary',
|
||||
'isCat': 'misskey:isCat',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
} satisfies Context;
|
||||
|
||||
export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition];
|
||||
|
||||
export const PRELOADED_CONTEXTS: Record<string, JsonLd> = {
|
||||
'https://w3id.org/identity/v1': id_v1,
|
||||
'https://w3id.org/security/v1': security_v1,
|
||||
'https://www.w3.org/ns/activitystreams': activitystreams,
|
||||
|
||||
39
packages/backend/src/core/activitypub/misc/validator.ts
Normal file
39
packages/backend/src/core/activitypub/misc/validator.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Response } from 'node-fetch';
|
||||
|
||||
export function validateContentTypeSetAsActivityPub(response: Response): void {
|
||||
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
||||
|
||||
if (contentType === '') {
|
||||
throw new Error('Validate content type of AP response: No content-type header');
|
||||
}
|
||||
if (
|
||||
contentType.startsWith('application/activity+json') ||
|
||||
(contentType.startsWith('application/ld+json;') && contentType.includes('https://www.w3.org/ns/activitystreams'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json');
|
||||
}
|
||||
|
||||
const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/;
|
||||
|
||||
export function validateContentTypeSetAsJsonLD(response: Response): void {
|
||||
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
||||
|
||||
if (contentType === '') {
|
||||
throw new Error('Validate content type of JSON LD: No content-type header');
|
||||
}
|
||||
if (
|
||||
contentType.startsWith('application/ld+json') ||
|
||||
contentType.startsWith('application/json') ||
|
||||
plusJsonSuffixRegex.test(contentType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json');
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import { ApResolverService } from '../ApResolverService.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import type { IObject } from '../type.js';
|
||||
import { isDocument, type IObject } from '../type.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApImageService {
|
||||
@@ -39,7 +39,7 @@ export class ApImageService {
|
||||
* Imageを作成します。
|
||||
*/
|
||||
@bindThis
|
||||
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
|
||||
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
|
||||
// 投稿者が凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
throw new Error('actor has been suspended');
|
||||
@@ -47,16 +47,18 @@ export class ApImageService {
|
||||
|
||||
const image = await this.apResolverService.createResolver().resolve(value);
|
||||
|
||||
if (!isDocument(image)) return null;
|
||||
|
||||
if (image.url == null) {
|
||||
throw new Error('invalid image: url not provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof image.url !== 'string') {
|
||||
throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!checkHttps(image.url)) {
|
||||
throw new Error('invalid image: unexpected schema of url: ' + image.url);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.info(`Creating the Image: ${image.url}`);
|
||||
@@ -86,12 +88,11 @@ export class ApImageService {
|
||||
/**
|
||||
* Imageを解決します。
|
||||
*
|
||||
* Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
|
||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||
* ImageをリモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||
*/
|
||||
@bindThis
|
||||
public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
|
||||
// TODO
|
||||
public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
|
||||
// TODO: Misskeyに対象のImageが登録されていればそれを返す
|
||||
|
||||
// リモートサーバーからフェッチしてきて登録
|
||||
return await this.createImage(actor, value);
|
||||
|
||||
@@ -27,7 +27,7 @@ export class ApMentionService {
|
||||
const limit = promiseLimit<MiUser | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
|
||||
)).filter((x): x is MiUser => x != null);
|
||||
)).filter(x => x != null);
|
||||
|
||||
return mentionedUsers;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
|
||||
@@ -24,6 +23,7 @@ import { StatusError } from '@/misc/status-error.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { ApMfmService } from '../ApMfmService.js';
|
||||
@@ -80,20 +80,20 @@ export class ApNoteService {
|
||||
const expectHost = this.utilityService.extractDbHost(uri);
|
||||
|
||||
if (!validPost.includes(getApType(object))) {
|
||||
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
|
||||
}
|
||||
|
||||
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
|
||||
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
|
||||
}
|
||||
|
||||
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
|
||||
if (object.attributedTo && actualHost !== expectHost) {
|
||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||
}
|
||||
|
||||
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
|
||||
return new Error('invalid Note: published timestamp is malformed');
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -127,7 +127,7 @@ export class ApNoteService {
|
||||
value,
|
||||
object,
|
||||
});
|
||||
throw new Error('invalid note');
|
||||
throw err;
|
||||
}
|
||||
|
||||
const note = object as IPost;
|
||||
@@ -151,11 +151,47 @@ export class ApNoteService {
|
||||
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
|
||||
}
|
||||
|
||||
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser;
|
||||
const uri = getOneApId(note.attributedTo);
|
||||
|
||||
// 投稿者が凍結されていたらスキップ
|
||||
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
||||
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
|
||||
if (cachedActor && cachedActor.isSuspended) {
|
||||
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
||||
}
|
||||
|
||||
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
||||
const apHashtags = extractApHashtags(note.tag);
|
||||
|
||||
const cw = note.summary === '' ? null : note.summary;
|
||||
|
||||
// テキストのパース
|
||||
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 !== 'undefined') {
|
||||
text = note._misskey_content;
|
||||
} else if (typeof note.content === 'string') {
|
||||
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
||||
}
|
||||
|
||||
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||
|
||||
//#region Contents Check
|
||||
// 添付ファイルとユーザーをこのサーバーで登録する前に内容をチェックする
|
||||
/**
|
||||
* 禁止ワードチェック
|
||||
*/
|
||||
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||
if (hasProhibitedWords) {
|
||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
|
||||
|
||||
// 解決した投稿者が凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
throw new Error('actor has been suspended');
|
||||
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
||||
}
|
||||
|
||||
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
||||
@@ -170,19 +206,14 @@ export class ApNoteService {
|
||||
}
|
||||
}
|
||||
|
||||
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
||||
const apHashtags = extractApHashtags(note.tag);
|
||||
|
||||
// 添付ファイル
|
||||
// TODO: attachmentは必ずしもImageではない
|
||||
// TODO: attachmentは必ずしも配列ではない
|
||||
const limit = promiseLimit<MiDriveFile>(2);
|
||||
const files = (await Promise.all(toArray(note.attachment).map(attach => (
|
||||
limit(() => this.apImageService.resolveImage(actor, {
|
||||
...attach,
|
||||
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
|
||||
}))
|
||||
))));
|
||||
const files: MiDriveFile[] = [];
|
||||
|
||||
for (const attach of toArray(note.attachment)) {
|
||||
attach.sensitive ??= note.sensitive;
|
||||
const file = await this.apImageService.resolveImage(actor, attach);
|
||||
if (file) files.push(file);
|
||||
}
|
||||
|
||||
// リプライ
|
||||
const reply: MiNote | null = note.inReplyTo
|
||||
@@ -221,7 +252,7 @@ export class ApNoteService {
|
||||
}
|
||||
};
|
||||
|
||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
|
||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
|
||||
const results = await Promise.all(uris.map(tryResolveNote));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||
@@ -232,18 +263,6 @@ export class ApNoteService {
|
||||
}
|
||||
}
|
||||
|
||||
const cw = note.summary === '' ? null : note.summary;
|
||||
|
||||
// テキストのパース
|
||||
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 !== 'undefined') {
|
||||
text = note._misskey_content;
|
||||
} else if (typeof note.content === 'string') {
|
||||
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
||||
}
|
||||
|
||||
// vote
|
||||
if (reply && reply.hasPoll) {
|
||||
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
|
||||
@@ -273,8 +292,6 @@ export class ApNoteService {
|
||||
|
||||
const apEmojis = emojis.map(emoji => emoji.name);
|
||||
|
||||
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||
|
||||
try {
|
||||
return await this.noteCreateService.create(actor, {
|
||||
createdAt: note.published ? new Date(note.published) : null,
|
||||
@@ -389,7 +406,7 @@ export class ApNoteService {
|
||||
|
||||
this.logger.info(`register emoji host=${host}, name=${name}`);
|
||||
|
||||
return await this.emojisRepository.insert({
|
||||
return await this.emojisRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
host,
|
||||
name,
|
||||
@@ -398,7 +415,7 @@ export class ApNoteService {
|
||||
publicUrl: tag.icon.url,
|
||||
updatedAt: new Date(),
|
||||
aliases: [],
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,7 +636,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
// とりあえずidを別の時間で生成して順番を維持
|
||||
let td = 0;
|
||||
for (const note of featuredNotes.filter((note): note is MiNote => note != null)) {
|
||||
for (const note of featuredNotes.filter(x => x != null)) {
|
||||
td -= 1000;
|
||||
transactionalEntityManager.insert(MiUserNotePining, {
|
||||
id: this.idService.gen(Date.now() + td),
|
||||
|
||||
@@ -51,7 +51,7 @@ export class ApQuestionService {
|
||||
|
||||
const choices = question[multiple ? 'anyOf' : 'oneOf']
|
||||
?.map((x) => x.name)
|
||||
.filter((x): x is string => typeof x === 'string')
|
||||
.filter(x => x != null)
|
||||
?? [];
|
||||
|
||||
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
|
||||
|
||||
@@ -15,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
|
||||
return hashtags.map(tag => {
|
||||
const m = tag.name.match(/^#(.+)/);
|
||||
return m ? m[1] : null;
|
||||
}).filter((x): x is string => x != null);
|
||||
}).filter(x => x != null);
|
||||
}
|
||||
|
||||
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface IObject {
|
||||
endTime?: Date;
|
||||
icon?: any;
|
||||
image?: any;
|
||||
mediaType?: string;
|
||||
url?: ApObject | string;
|
||||
href?: string;
|
||||
tag?: IObject | IObject[];
|
||||
@@ -240,14 +241,14 @@ export interface IKey extends IObject {
|
||||
}
|
||||
|
||||
export interface IApDocument extends IObject {
|
||||
type: 'Document';
|
||||
name: string | null;
|
||||
mediaType: string;
|
||||
type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
|
||||
}
|
||||
|
||||
export interface IApImage extends IObject {
|
||||
export const isDocument = (object: IObject): object is IApDocument =>
|
||||
['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
|
||||
|
||||
export interface IApImage extends IApDocument {
|
||||
type: 'Image';
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
export interface ICreate extends IActivity {
|
||||
@@ -327,3 +328,4 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob
|
||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
|
||||
export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
|
||||
|
||||
@@ -14,6 +14,6 @@ export class ChartLoggerService {
|
||||
constructor(
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test');
|
||||
this.logger = this.loggerService.getLogger('chart', 'white');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||
|
||||
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('instance.host')
|
||||
.where('instance.isSuspended = true');
|
||||
.where('instance.suspensionState != \'none\'');
|
||||
|
||||
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
|
||||
.select('f.followerHost')
|
||||
@@ -89,7 +89,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.isSuspended = false')
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
@@ -97,7 +97,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.isSuspended = false')
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user