Merge branch 'develop' into fix-7311
This commit is contained in:
commit
0f7ee33201
877 changed files with 33614 additions and 11918 deletions
31
packages/backend/assets/embed.js
Normal file
31
packages/backend/assets/embed.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
//@ts-check
|
||||
(() => {
|
||||
/** @type {NodeListOf<HTMLIFrameElement>} */
|
||||
const els = document.querySelectorAll('iframe[data-misskey-embed-id]');
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
els.forEach((el) => {
|
||||
if (event.source !== el.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = el.dataset.misskeyEmbedId;
|
||||
|
||||
if (event.data.type === 'misskey:embed:ready') {
|
||||
el.contentWindow?.postMessage({
|
||||
type: 'misskey:embedParent:registerIframeId',
|
||||
payload: {
|
||||
iframeId: id,
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
if (event.data.type === 'misskey:embed:changeHeight' && event.data.iframeId === id) {
|
||||
el.style.height = event.data.payload.height + 'px';
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
BIN
packages/backend/assets/tabler-badges/login-2.png
Normal file
BIN
packages/backend/assets/tabler-badges/login-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
|
|
@ -11,7 +11,7 @@ export default [
|
|||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
project: ['./tsconfig.json', './test/tsconfig.json'],
|
||||
project: ['./tsconfig.json', './test/tsconfig.json', './test-federation/tsconfig.json'],
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
|
|
|
|||
13
packages/backend/jest.config.fed.cjs
Normal file
13
packages/backend/jest.config.fed.cjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* For a detailed explanation regarding each configuration property and type check, visit:
|
||||
* https://jestjs.io/docs/en/configuration.html
|
||||
*/
|
||||
|
||||
const base = require('./jest.config.cjs');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
testMatch: [
|
||||
'<rootDir>/test-federation/test/**/*.test.ts',
|
||||
],
|
||||
};
|
||||
16
packages/backend/migration/1723944246767-followedMessage.js
Normal file
16
packages/backend/migration/1723944246767-followedMessage.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class FollowedMessage1723944246767 {
|
||||
name = 'FollowedMessage1723944246767';
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)');
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ReactionsBuffering1726804538569 {
|
||||
name = 'ReactionsBuffering1726804538569'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableReactionsBuffering" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableReactionsBuffering"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class EnableStatsForFederatedInstances1727318020265 {
|
||||
name = 'EnableStatsForFederatedInstances1727318020265'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableStatsForFederatedInstances" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableStatsForFederatedInstances"`);
|
||||
}
|
||||
}
|
||||
16
packages/backend/migration/1727491883993-user-score.js
Normal file
16
packages/backend/migration/1727491883993-user-score.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class UserScore1727491883993 {
|
||||
name = 'UserScore1727491883993'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "score" integer NOT NULL DEFAULT '0'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "score"`);
|
||||
}
|
||||
}
|
||||
18
packages/backend/migration/1727512908322-meta-federation.js
Normal file
18
packages/backend/migration/1727512908322-meta-federation.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class MetaFederation1727512908322 {
|
||||
name = 'MetaFederation1727512908322'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "federation" character varying(128) NOT NULL DEFAULT 'all'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "federationHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federationHosts"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federation"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RefineAbuseUserReport1728085812127 {
|
||||
name = 'RefineAbuseUserReport1728085812127'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`);
|
||||
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`);
|
||||
}
|
||||
}
|
||||
16
packages/backend/migration/1728550878802-testcaptcha.js
Normal file
16
packages/backend/migration/1728550878802-testcaptcha.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Testcaptcha1728550878802 {
|
||||
name = 'Testcaptcha1728550878802'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ProhibitedWordsForNameOfUser1728634286056 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWordsForNameOfUser" character varying(1024) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWordsForNameOfUser"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class SigninRequiredForShowContents1729333924409 {
|
||||
name = 'SigninRequiredForShowContents1729333924409'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "requireSigninToViewContents" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "requireSigninToViewContents"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class MakeNotesHiddenBefore1729486255072 {
|
||||
name = 'MakeNotesHiddenBefore1729486255072'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesFollowersOnlyBefore" integer`);
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesHiddenBefore" integer`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesHiddenBefore"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesFollowersOnlyBefore"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,16 +19,18 @@
|
|||
"watch": "node ./scripts/watch.mjs",
|
||||
"restart": "pnpm build && pnpm start",
|
||||
"dev": "node ./scripts/dev.mjs",
|
||||
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
|
||||
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
||||
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
|
||||
"eslint": "eslint --quiet \"{src,test-federation}/**/*.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",
|
||||
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
|
||||
"jest:fed": "node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.fed.cjs",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
||||
"test": "pnpm jest",
|
||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test:fed": "pnpm jest:fed",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"generate-api-json": "node ./scripts/generate_api_json.js"
|
||||
|
|
@ -67,24 +69,24 @@
|
|||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.620.0",
|
||||
"@aws-sdk/lib-storage": "3.620.0",
|
||||
"@bull-board/api": "5.21.1",
|
||||
"@bull-board/fastify": "5.21.1",
|
||||
"@bull-board/ui": "5.21.1",
|
||||
"@discordapp/twemoji": "15.0.3",
|
||||
"@fastify/accepts": "4.3.0",
|
||||
"@fastify/cookie": "9.3.1",
|
||||
"@fastify/cors": "9.0.1",
|
||||
"@fastify/express": "3.0.0",
|
||||
"@fastify/http-proxy": "9.5.0",
|
||||
"@fastify/multipart": "8.3.0",
|
||||
"@fastify/static": "7.0.4",
|
||||
"@fastify/view": "9.1.0",
|
||||
"@bull-board/api": "6.0.0",
|
||||
"@bull-board/fastify": "6.0.0",
|
||||
"@bull-board/ui": "6.0.0",
|
||||
"@discordapp/twemoji": "15.1.0",
|
||||
"@fastify/accepts": "5.0.1",
|
||||
"@fastify/cookie": "10.0.1",
|
||||
"@fastify/cors": "10.0.1",
|
||||
"@fastify/express": "4.0.1",
|
||||
"@fastify/http-proxy": "10.0.0",
|
||||
"@fastify/multipart": "9.0.1",
|
||||
"@fastify/static": "8.0.1",
|
||||
"@fastify/view": "10.0.1",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.1.0",
|
||||
"@napi-rs/canvas": "^0.1.53",
|
||||
"@nestjs/common": "10.3.10",
|
||||
"@nestjs/core": "10.3.10",
|
||||
"@nestjs/testing": "10.3.10",
|
||||
"@napi-rs/canvas": "0.1.56",
|
||||
"@nestjs/common": "10.4.4",
|
||||
"@nestjs/core": "10.4.4",
|
||||
"@nestjs/testing": "10.4.4",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "8.20.0",
|
||||
"@sentry/profiling-node": "8.20.0",
|
||||
|
|
@ -100,8 +102,8 @@
|
|||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.2",
|
||||
"bullmq": "5.10.4",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.15.0",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"chalk": "5.3.0",
|
||||
|
|
@ -112,27 +114,28 @@
|
|||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "4.28.1",
|
||||
"fastify-raw-body": "4.3.0",
|
||||
"fastify": "5.0.0",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.3.0",
|
||||
"file-type": "19.5.0",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.4.2",
|
||||
"happy-dom": "10.0.3",
|
||||
"happy-dom": "15.7.4",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
"ioredis": "5.4.1",
|
||||
"ip-cidr": "4.0.1",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "5.0.1",
|
||||
"is-svg": "5.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "24.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.2",
|
||||
"jsrsasign": "11.1.0",
|
||||
"meilisearch": "0.41.0",
|
||||
"meilisearch": "0.42.0",
|
||||
"juice": "11.0.0",
|
||||
"mfm-js": "0.24.0",
|
||||
"microformats-parser": "2.0.2",
|
||||
"mime-types": "2.1.35",
|
||||
|
|
@ -142,42 +145,42 @@
|
|||
"nanoid": "5.0.7",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.14",
|
||||
"nodemailer": "6.9.15",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "0.10.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.3.1",
|
||||
"otpauth": "9.3.4",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.12.0",
|
||||
"pg": "8.13.0",
|
||||
"pkce-challenge": "4.1.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.3",
|
||||
"punycode": "2.3.1",
|
||||
"qrcode": "1.5.3",
|
||||
"qrcode": "1.5.4",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.21.3",
|
||||
"re2": "1.21.4",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.1",
|
||||
"sanitize-html": "2.13.0",
|
||||
"sanitize-html": "2.13.1",
|
||||
"secure-json-parse": "2.7.0",
|
||||
"sharp": "0.33.4",
|
||||
"sharp": "0.33.5",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.22.11",
|
||||
"systeminformation": "5.23.5",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.20",
|
||||
"typescript": "5.5.4",
|
||||
"typescript": "5.6.2",
|
||||
"ulid": "2.3.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
|
|
@ -186,19 +189,19 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@nestjs/platform-express": "10.3.10",
|
||||
"@nestjs/platform-express": "10.4.4",
|
||||
"@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/color-convert": "2.0.3",
|
||||
"@types/color-convert": "2.0.4",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/fluent-ffmpeg": "2.1.24",
|
||||
"@types/fluent-ffmpeg": "2.1.26",
|
||||
"@types/htmlescape": "1.1.3",
|
||||
"@types/http-link-header": "1.0.7",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jest": "29.5.13",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/jsonld": "1.5.15",
|
||||
|
|
@ -206,18 +209,18 @@
|
|||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.14.12",
|
||||
"@types/nodemailer": "6.4.15",
|
||||
"@types/nodemailer": "6.4.16",
|
||||
"@types/oauth": "0.9.5",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.11.6",
|
||||
"@types/pg": "8.11.10",
|
||||
"@types/pug": "2.0.10",
|
||||
"@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.11.0",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
|
|
@ -225,17 +228,17 @@
|
|||
"@types/tmp": "0.2.6",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.3",
|
||||
"@types/ws": "8.5.11",
|
||||
"@types/ws": "8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"aws-sdk-client-mock": "4.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"execa": "9.3.0",
|
||||
"eslint-plugin-import": "2.30.0",
|
||||
"execa": "9.4.0",
|
||||
"fkill": "9.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.1.4",
|
||||
"nodemon": "3.1.7",
|
||||
"pid-port": "1.0.0",
|
||||
"simple-oauth2": "5.1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,52 @@
|
|||
|
||||
import Redis from 'ioredis';
|
||||
import { loadConfig } from '../built/config.js';
|
||||
import { createPostgresDataSource } from '../built/postgres.js';
|
||||
|
||||
const config = loadConfig();
|
||||
const redis = new Redis(config.redis);
|
||||
|
||||
redis.on('connect', () => redis.disconnect());
|
||||
redis.on('error', (e) => {
|
||||
throw e;
|
||||
});
|
||||
async function connectToPostgres() {
|
||||
const source = createPostgresDataSource(config);
|
||||
await source.initialize();
|
||||
await source.destroy();
|
||||
}
|
||||
|
||||
async function connectToRedis(redisOptions) {
|
||||
return await new Promise(async (resolve, reject) => {
|
||||
const redis = new Redis({
|
||||
...redisOptions,
|
||||
lazyConnect: true,
|
||||
reconnectOnError: false,
|
||||
showFriendlyErrorStack: true,
|
||||
});
|
||||
redis.on('error', e => reject(e));
|
||||
|
||||
try {
|
||||
await redis.connect();
|
||||
resolve();
|
||||
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
|
||||
} finally {
|
||||
redis.disconnect(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If not all of these are defined, the default one gets reused.
|
||||
// so we use a Set to only try connecting once to each **uniq** redis.
|
||||
const promises = Array
|
||||
.from(new Set([
|
||||
config.redis,
|
||||
config.redisForPubsub,
|
||||
config.redisForJobQueue,
|
||||
config.redisForTimelines,
|
||||
config.redisForReactions,
|
||||
]))
|
||||
.map(connectToRedis)
|
||||
.concat([
|
||||
connectToPostgres()
|
||||
]);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { createPostgresDataSource } from './postgres.js';
|
|||
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||
import { allSettled } from './misc/promise-tracker.js';
|
||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
import { GlobalEvents } from './core/GlobalEventService.js';
|
||||
|
||||
const $config: Provider = {
|
||||
provide: DI.config,
|
||||
|
|
@ -78,11 +80,76 @@ const $redisForTimelines: Provider = {
|
|||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $redisForReactions: Provider = {
|
||||
provide: DI.redisForReactions,
|
||||
useFactory: (config: Config) => {
|
||||
return new Redis.Redis(config.redisForReactions);
|
||||
},
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $meta: Provider = {
|
||||
provide: DI.meta,
|
||||
useFactory: async (db: DataSource, redisForSub: Redis.Redis) => {
|
||||
const meta = await db.transaction(async transactionalEntityManager => {
|
||||
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||
const metas = await transactionalEntityManager.find(MiMeta, {
|
||||
order: {
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
const meta = metas[0];
|
||||
|
||||
if (meta) {
|
||||
return meta;
|
||||
} else {
|
||||
// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
|
||||
const saved = await transactionalEntityManager
|
||||
.upsert(
|
||||
MiMeta,
|
||||
{
|
||||
id: 'x',
|
||||
},
|
||||
['id'],
|
||||
)
|
||||
.then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0]));
|
||||
|
||||
return saved;
|
||||
}
|
||||
});
|
||||
|
||||
async function 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 'metaUpdated': {
|
||||
for (const key in body.after) {
|
||||
(meta as any)[key] = (body.after as any)[key];
|
||||
}
|
||||
meta.proxyAccount = null; // joinなカラムは通常取ってこないので
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redisForSub.on('message', onMessage);
|
||||
|
||||
return meta;
|
||||
},
|
||||
inject: [DI.db, DI.redisForSub],
|
||||
};
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [RepositoryModule],
|
||||
providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
|
||||
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
|
||||
providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions],
|
||||
exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule],
|
||||
})
|
||||
export class GlobalModule implements OnApplicationShutdown {
|
||||
constructor(
|
||||
|
|
@ -91,6 +158,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||
@Inject(DI.redisForReactions) private redisForReactions: Redis.Redis,
|
||||
) { }
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
|
|
@ -103,6 +171,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||
this.redisForPub.disconnect(),
|
||||
this.redisForSub.disconnect(),
|
||||
this.redisForTimelines.disconnect(),
|
||||
this.redisForReactions.disconnect(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ type Source = {
|
|||
redisForPubsub?: RedisOptionsSource;
|
||||
redisForJobQueue?: RedisOptionsSource;
|
||||
redisForTimelines?: RedisOptionsSource;
|
||||
redisForReactions?: RedisOptionsSource;
|
||||
meilisearch?: {
|
||||
host: string;
|
||||
port: string;
|
||||
|
|
@ -62,6 +63,8 @@ type Source = {
|
|||
|
||||
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
|
||||
|
||||
setupPassword?: string;
|
||||
|
||||
proxy?: string;
|
||||
proxySmtp?: string;
|
||||
proxyBypassHosts?: string[];
|
||||
|
|
@ -151,6 +154,7 @@ export type Config = {
|
|||
|
||||
version: string;
|
||||
publishTarballInsteadOfProvideRepositoryUrl: boolean;
|
||||
setupPassword: string | undefined;
|
||||
host: string;
|
||||
hostname: string;
|
||||
scheme: string;
|
||||
|
|
@ -160,8 +164,10 @@ export type Config = {
|
|||
authUrl: string;
|
||||
driveUrl: string;
|
||||
userAgent: string;
|
||||
clientEntry: string;
|
||||
clientManifestExists: boolean;
|
||||
frontendEntry: string;
|
||||
frontendManifestExists: boolean;
|
||||
frontendEmbedEntry: string;
|
||||
frontendEmbedManifestExists: boolean;
|
||||
mediaProxy: string;
|
||||
externalMediaProxyEnabled: boolean;
|
||||
videoThumbnailGenerator: string | null;
|
||||
|
|
@ -169,6 +175,7 @@ export type Config = {
|
|||
redisForPubsub: RedisOptions & RedisOptionsSource;
|
||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||
redisForReactions: RedisOptions & RedisOptionsSource;
|
||||
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
||||
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
|
||||
perChannelMaxNoteCacheCount: number;
|
||||
|
|
@ -196,10 +203,16 @@ const path = process.env.MISSKEY_CONFIG_YML
|
|||
|
||||
export function loadConfig(): Config {
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
|
||||
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
|
||||
const clientManifest = clientManifestExists ?
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
|
||||
|
||||
const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
|
||||
const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
|
||||
const frontendManifest = frontendManifestExists ?
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
|
||||
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
|
||||
const frontendEmbedManifest = frontendEmbedManifestExists ?
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
|
||||
: { 'src/boot.ts': { file: 'src/boot.ts' } };
|
||||
|
||||
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
||||
|
||||
const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
|
||||
|
|
@ -222,6 +235,7 @@ export function loadConfig(): Config {
|
|||
return {
|
||||
version,
|
||||
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
|
||||
setupPassword: config.setupPassword,
|
||||
url: url.origin,
|
||||
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
|
||||
socket: config.socket,
|
||||
|
|
@ -243,6 +257,7 @@ 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,
|
||||
redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
|
||||
sentryForBackend: config.sentryForBackend,
|
||||
sentryForFrontend: config.sentryForFrontend,
|
||||
id: config.id,
|
||||
|
|
@ -270,8 +285,10 @@ export function loadConfig(): Config {
|
|||
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
||||
: null,
|
||||
userAgent: `Misskey/${version} (${config.url})`,
|
||||
clientEntry: clientManifest['src/_boot_.ts'],
|
||||
clientManifestExists: clientManifestExists,
|
||||
frontendEntry: frontendManifest['src/_boot_.ts'],
|
||||
frontendManifestExists: frontendManifestExists,
|
||||
frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
|
||||
frontendEmbedManifestExists: frontendEmbedManifestExists,
|
||||
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export const MAX_NOTE_TEXT_LENGTH = 3000;
|
|||
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
||||
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
|
||||
|
||||
export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||
|
||||
//#region hard limits
|
||||
// If you change DB_* values, you must also change the DB schema.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,30 +14,36 @@ import type {
|
|||
AbuseReportNotificationRecipientRepository,
|
||||
MiAbuseReportNotificationRecipient,
|
||||
MiAbuseUserReport,
|
||||
MiMeta,
|
||||
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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { IdService } from './IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AbuseReportNotificationService implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@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,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
|
@ -55,7 +61,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
return;
|
||||
}
|
||||
|
||||
const moderatorIds = await this.roleService.getModeratorIds(true, true);
|
||||
const moderatorIds = await this.roleService.getModeratorIds({
|
||||
includeAdmins: true,
|
||||
excludeExpire: true,
|
||||
});
|
||||
|
||||
for (const moderatorId of moderatorIds) {
|
||||
for (const abuseReport of abuseReports) {
|
||||
|
|
@ -93,10 +102,8 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
.filter(x => x != null),
|
||||
);
|
||||
|
||||
// 送信先の鮮度を保つため、毎回取得する
|
||||
const meta = await this.metaService.fetch(true);
|
||||
recipientEMailAddresses.push(
|
||||
...(meta.email ? [meta.email] : []),
|
||||
...(this.meta.email ? [this.meta.email] : []),
|
||||
);
|
||||
|
||||
if (recipientEMailAddresses.length <= 0) {
|
||||
|
|
@ -133,6 +140,26 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
return;
|
||||
}
|
||||
|
||||
const usersMap = await this.userEntityService.packMany(
|
||||
[
|
||||
...new Set([
|
||||
...abuseReports.map(it => it.reporter ?? it.reporterId),
|
||||
...abuseReports.map(it => it.targetUser ?? it.targetUserId),
|
||||
...abuseReports.map(it => it.assignee ?? it.assigneeId),
|
||||
].filter(x => x != null)),
|
||||
],
|
||||
null,
|
||||
{ schema: 'UserLite' },
|
||||
).then(it => new Map(it.map(it => [it.id, it])));
|
||||
const convertedReports = abuseReports.map(it => {
|
||||
return {
|
||||
...it,
|
||||
reporter: usersMap.get(it.reporterId),
|
||||
targetUser: usersMap.get(it.targetUserId),
|
||||
assignee: it.assigneeId ? usersMap.get(it.assigneeId) : null,
|
||||
};
|
||||
});
|
||||
|
||||
const recipientWebhookIds = await this.fetchWebhookRecipients()
|
||||
.then(it => it
|
||||
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
|
||||
|
|
@ -140,7 +167,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
.filter(x => x != null));
|
||||
for (const webhookId of recipientWebhookIds) {
|
||||
await Promise.all(
|
||||
abuseReports.map(it => {
|
||||
convertedReports.map(it => {
|
||||
return this.systemWebhookService.enqueueSystemWebhook(
|
||||
webhookId,
|
||||
type,
|
||||
|
|
@ -261,8 +288,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
.log(updater, 'createAbuseReportNotificationRecipient', {
|
||||
recipientId: id,
|
||||
recipient: created,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
|
||||
return created;
|
||||
}
|
||||
|
|
@ -300,8 +326,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
recipientId: params.id,
|
||||
before: beforeEntity,
|
||||
after: afterEntity,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
|
||||
return afterEntity;
|
||||
}
|
||||
|
|
@ -322,8 +347,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
.log(updater, 'deleteAbuseReportNotificationRecipient', {
|
||||
recipientId: id,
|
||||
recipient: entity,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -346,7 +370,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
// モデレータ権限の有無で通知先設定を振り分ける
|
||||
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
|
||||
const authorizedUserIds = await this.roleService.getModeratorIds({
|
||||
includeAdmins: true,
|
||||
excludeExpire: true,
|
||||
});
|
||||
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||
for (const recipient of userRecipients) {
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ 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,
|
||||
|
|
@ -77,16 +79,16 @@ export class AbuseReportService {
|
|||
* - SystemWebhook
|
||||
*
|
||||
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||
* @param operator 通報を処理したユーザ
|
||||
* @param moderator 通報を処理したユーザ
|
||||
* @see AbuseReportNotificationService.notify
|
||||
*/
|
||||
@bindThis
|
||||
public async resolve(
|
||||
params: {
|
||||
reportId: string;
|
||||
forward: boolean;
|
||||
resolvedAs: MiAbuseUserReport['resolvedAs'];
|
||||
}[],
|
||||
operator: MiUser,
|
||||
moderator: MiUser,
|
||||
) {
|
||||
const paramsMap = new Map(params.map(it => [it.reportId, it]));
|
||||
const reports = await this.abuseUserReportsRepository.findBy({
|
||||
|
|
@ -99,30 +101,76 @@ export class AbuseReportService {
|
|||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
resolved: true,
|
||||
assigneeId: operator.id,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
assigneeId: moderator.id,
|
||||
resolvedAs: ps.resolvedAs,
|
||||
});
|
||||
|
||||
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', {
|
||||
.log(moderator, 'resolveAbuseReport', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
})
|
||||
.then();
|
||||
resolvedAs: ps.resolvedAs,
|
||||
});
|
||||
}
|
||||
|
||||
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
|
||||
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async forward(
|
||||
reportId: MiAbuseUserReport['id'],
|
||||
moderator: MiUser,
|
||||
) {
|
||||
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
|
||||
|
||||
if (report.targetUserHost == null) {
|
||||
throw new Error('The target user host is null.');
|
||||
}
|
||||
|
||||
if (report.forwarded) {
|
||||
throw new Error('The report has already been forwarded.');
|
||||
}
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
forwarded: true,
|
||||
});
|
||||
|
||||
const actor = await this.instanceActorService.getInstanceActor();
|
||||
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
||||
|
||||
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(moderator, 'forwardAbuseReport', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
reportId: MiAbuseUserReport['id'],
|
||||
params: {
|
||||
moderationNote?: MiAbuseUserReport['moderationNote'];
|
||||
},
|
||||
moderator: MiUser,
|
||||
) {
|
||||
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
moderationNote: params.moderationNote,
|
||||
});
|
||||
|
||||
if (params.moderationNote != null && report.moderationNote !== params.moderationNote) {
|
||||
this.moderationLogService.log(moderator, 'updateAbuseReportNote', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
before: report.moderationNote,
|
||||
after: params.moderationNote,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
|
@ -22,13 +22,15 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
|||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
|
||||
@Injectable()
|
||||
export class AccountMoveService {
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
|
@ -57,7 +59,6 @@ export class AccountMoveService {
|
|||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private instanceChart: InstanceChart,
|
||||
private metaService: MetaService,
|
||||
private relayService: RelayService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
|
|
@ -273,13 +274,15 @@ export class AccountMoveService {
|
|||
}
|
||||
|
||||
// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
|
||||
if (this.userEntityService.isRemoteUser(oldAccount)) {
|
||||
this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(oldAccount)) {
|
||||
this.federatedInstanceService.fetchOrRegister(oldAccount.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: expensive?
|
||||
|
|
|
|||
|
|
@ -209,6 +209,13 @@ export class AnnouncementService {
|
|||
return;
|
||||
}
|
||||
|
||||
const announcement = await this.announcementsRepository.findOneBy({ id: announcementId });
|
||||
if (announcement != null && announcement.userId === user.id) {
|
||||
await this.announcementsRepository.update(announcementId, {
|
||||
isActive: false,
|
||||
});
|
||||
}
|
||||
|
||||
if ((await this.getUnreadAnnouncements(user)).length === 0) {
|
||||
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
if (antenna.src === 'home') {
|
||||
// TODO
|
||||
} else if (antenna.src === 'list') {
|
||||
const listUsers = (await this.userListMembershipsRepository.findBy({
|
||||
userListId: antenna.userListId!,
|
||||
})).map(x => x.userId);
|
||||
|
||||
if (!listUsers.includes(note.userId)) return false;
|
||||
if (antenna.userListId == null) return false;
|
||||
const exists = await this.userListMembershipsRepository.exists({
|
||||
where: {
|
||||
userListId: antenna.userListId,
|
||||
userId: note.userId,
|
||||
},
|
||||
});
|
||||
if (!exists) return false;
|
||||
} else if (antenna.src === 'users') {
|
||||
const accts = antenna.users.map(x => {
|
||||
const { username, host } = Acct.parse(x);
|
||||
|
|
|
|||
|
|
@ -119,5 +119,18 @@ export class CaptchaService {
|
|||
throw new Error(`turnstile-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw new Error('testcaptcha-failed: no response provided');
|
||||
}
|
||||
|
||||
const success = response === 'testcaptcha-passed';
|
||||
|
||||
if (!success) {
|
||||
throw new Error('testcaptcha-failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import {
|
|||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||
import { FlashService } from '@/core/FlashService.js';
|
||||
import { AccountMoveService } from './AccountMoveService.js';
|
||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AiService } from './AiService.js';
|
||||
|
|
@ -49,6 +51,7 @@ import { PollService } from './PollService.js';
|
|||
import { PushNotificationService } from './PushNotificationService.js';
|
||||
import { QueryService } from './QueryService.js';
|
||||
import { ReactionService } from './ReactionService.js';
|
||||
import { ReactionsBufferingService } from './ReactionsBufferingService.js';
|
||||
import { RelayService } from './RelayService.js';
|
||||
import { RoleService } from './RoleService.js';
|
||||
import { S3Service } from './S3Service.js';
|
||||
|
|
@ -192,6 +195,7 @@ const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExis
|
|||
const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
|
||||
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
|
||||
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
|
||||
const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingService', useExisting: ReactionsBufferingService };
|
||||
const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
|
||||
const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
|
||||
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
|
||||
|
|
@ -211,8 +215,10 @@ const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: Us
|
|||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
||||
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
|
||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||
const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
|
||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
||||
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
|
||||
|
|
@ -340,6 +346,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
PushNotificationService,
|
||||
QueryService,
|
||||
ReactionService,
|
||||
ReactionsBufferingService,
|
||||
RelayService,
|
||||
RoleService,
|
||||
S3Service,
|
||||
|
|
@ -359,8 +366,10 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
VideoProcessingService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
WebhookTestService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
FlashService,
|
||||
SearchService,
|
||||
ClipService,
|
||||
FeaturedService,
|
||||
|
|
@ -484,6 +493,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$PushNotificationService,
|
||||
$QueryService,
|
||||
$ReactionService,
|
||||
$ReactionsBufferingService,
|
||||
$RelayService,
|
||||
$RoleService,
|
||||
$S3Service,
|
||||
|
|
@ -503,8 +513,10 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$VideoProcessingService,
|
||||
$UserWebhookService,
|
||||
$SystemWebhookService,
|
||||
$WebhookTestService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$FlashService,
|
||||
$SearchService,
|
||||
$ClipService,
|
||||
$FeaturedService,
|
||||
|
|
@ -629,6 +641,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
PushNotificationService,
|
||||
QueryService,
|
||||
ReactionService,
|
||||
ReactionsBufferingService,
|
||||
RelayService,
|
||||
RoleService,
|
||||
S3Service,
|
||||
|
|
@ -648,8 +661,10 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
VideoProcessingService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
WebhookTestService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
FlashService,
|
||||
SearchService,
|
||||
ClipService,
|
||||
FeaturedService,
|
||||
|
|
@ -772,6 +787,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$PushNotificationService,
|
||||
$QueryService,
|
||||
$ReactionService,
|
||||
$ReactionsBufferingService,
|
||||
$RelayService,
|
||||
$RoleService,
|
||||
$S3Service,
|
||||
|
|
@ -791,6 +807,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$VideoProcessingService,
|
||||
$UserWebhookService,
|
||||
$SystemWebhookService,
|
||||
$WebhookTestService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
|
|
|
|||
|
|
@ -103,19 +103,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async update(id: MiEmoji['id'], data: {
|
||||
public async update(data: (
|
||||
{ id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
|
||||
) & {
|
||||
driveFile?: MiDriveFile;
|
||||
name?: string;
|
||||
category?: string | null;
|
||||
aliases?: string[];
|
||||
license?: string | null;
|
||||
isSensitive?: boolean;
|
||||
localOnly?: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
|
||||
}, moderator?: MiUser): Promise<void> {
|
||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
||||
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
||||
}, moderator?: MiUser): Promise<
|
||||
null
|
||||
| 'NO_SUCH_EMOJI'
|
||||
| 'SAME_NAME_EMOJI_EXISTS'
|
||||
> {
|
||||
const emoji = data.id
|
||||
? await this.getEmojiById(data.id)
|
||||
: await this.getEmojiByName(data.name!);
|
||||
if (emoji === null) return 'NO_SUCH_EMOJI';
|
||||
const id = emoji.id;
|
||||
|
||||
// IDと絵文字名が両方指定されている場合は絵文字名の変更を行うため重複チェックが必要
|
||||
const doNameUpdate = data.id && data.name && (data.name !== emoji.name);
|
||||
if (doNameUpdate) {
|
||||
const isDuplicate = await this.checkDuplicate(data.name!);
|
||||
if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS';
|
||||
}
|
||||
|
||||
await this.emojisRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
|
|
@ -135,7 +149,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
|
||||
const packed = await this.emojiEntityService.packDetailed(emoji.id);
|
||||
|
||||
if (emoji.name === data.name) {
|
||||
if (!doNameUpdate) {
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: [packed],
|
||||
});
|
||||
|
|
@ -157,6 +171,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
after: updated,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -11,11 +11,10 @@ import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
|||
import { IsNull } from 'typeorm';
|
||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js';
|
||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import Logger from '@/logger.js';
|
||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
|
|
@ -99,6 +98,9 @@ export class DriveService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
|
@ -115,7 +117,6 @@ export class DriveService {
|
|||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private downloadService: DownloadService,
|
||||
private internalStorageService: InternalStorageService,
|
||||
private s3Service: S3Service,
|
||||
|
|
@ -149,9 +150,7 @@ export class DriveService {
|
|||
// thunbnail, webpublic を必要なら生成
|
||||
const alts = await this.generateAlts(path, type, !file.uri);
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
if (meta.useObjectStorage) {
|
||||
if (this.meta.useObjectStorage) {
|
||||
//#region ObjectStorage params
|
||||
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']);
|
||||
|
||||
|
|
@ -170,11 +169,11 @@ export class DriveService {
|
|||
ext = '';
|
||||
}
|
||||
|
||||
const baseUrl = meta.objectStorageBaseUrl
|
||||
?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
|
||||
const baseUrl = this.meta.objectStorageBaseUrl
|
||||
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
|
||||
|
||||
// for original
|
||||
const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
||||
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
||||
const url = `${ baseUrl }/${ key }`;
|
||||
|
||||
// for alts
|
||||
|
|
@ -191,7 +190,7 @@ export class DriveService {
|
|||
];
|
||||
|
||||
if (alts.webpublic) {
|
||||
webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||
webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
||||
|
||||
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
||||
|
|
@ -199,7 +198,7 @@ export class DriveService {
|
|||
}
|
||||
|
||||
if (alts.thumbnail) {
|
||||
thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||
thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
||||
|
||||
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
||||
|
|
@ -376,10 +375,8 @@ export class DriveService {
|
|||
if (type === 'image/apng') type = 'image/png';
|
||||
if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream';
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
const params = {
|
||||
Bucket: meta.objectStorageBucket,
|
||||
Bucket: this.meta.objectStorageBucket,
|
||||
Key: key,
|
||||
Body: stream,
|
||||
ContentType: type,
|
||||
|
|
@ -392,9 +389,9 @@ export class DriveService {
|
|||
// 許可されているファイル形式でしか拡張子をつけない
|
||||
ext ? correctFilename(filename, ext) : filename,
|
||||
);
|
||||
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
||||
if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
||||
|
||||
await this.s3Service.upload(meta, params)
|
||||
await this.s3Service.upload(this.meta, params)
|
||||
.then(
|
||||
result => {
|
||||
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
|
||||
|
|
@ -460,32 +457,31 @@ export class DriveService {
|
|||
ext = null,
|
||||
}: AddFileArgs): Promise<MiDriveFile> {
|
||||
let skipNsfwCheck = false;
|
||||
const instance = await this.metaService.fetch();
|
||||
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
|
||||
if (user == null) {
|
||||
skipNsfwCheck = true;
|
||||
} else if (userRoleNSFW) {
|
||||
skipNsfwCheck = true;
|
||||
}
|
||||
if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
|
||||
if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
|
||||
if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
|
||||
if (this.meta.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
|
||||
if (user && this.meta.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
|
||||
if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
|
||||
|
||||
const info = await this.fileInfoService.getFileInfo(path, {
|
||||
skipSensitiveDetection: skipNsfwCheck,
|
||||
sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる
|
||||
instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
|
||||
instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
|
||||
instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
|
||||
instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
|
||||
this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 :
|
||||
this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 :
|
||||
this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 :
|
||||
this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 :
|
||||
0.5,
|
||||
sensitiveThresholdForPorn: 0.75,
|
||||
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
||||
enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos,
|
||||
});
|
||||
this.registerLogger.info(`${JSON.stringify(info)}`);
|
||||
|
||||
// 現状 false positive が多すぎて実用に耐えない
|
||||
//if (info.porn && instance.disallowUploadWhenPredictedAsPorn) {
|
||||
//if (info.porn && this.meta.disallowUploadWhenPredictedAsPorn) {
|
||||
// throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.');
|
||||
//}
|
||||
|
||||
|
|
@ -589,9 +585,9 @@ export class DriveService {
|
|||
sensitive ?? false
|
||||
: false;
|
||||
|
||||
if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true;
|
||||
if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
|
||||
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
|
||||
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
|
||||
if (info.sensitive && this.meta.setSensitiveFlagAutomatically) file.isSensitive = true;
|
||||
if (userRoleNSFW) file.isSensitive = true;
|
||||
|
||||
if (url !== null) {
|
||||
|
|
@ -652,7 +648,7 @@ export class DriveService {
|
|||
// ローカルユーザーのみ
|
||||
this.perUserDriveChart.update(file, true);
|
||||
} else {
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateDrive(file, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -798,7 +794,7 @@ export class DriveService {
|
|||
// ローカルユーザーのみ
|
||||
this.perUserDriveChart.update(file, false);
|
||||
} else {
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateDrive(file, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -820,14 +816,13 @@ export class DriveService {
|
|||
|
||||
@bindThis
|
||||
public async deleteObjectStorageFile(key: string) {
|
||||
const meta = await this.metaService.fetch();
|
||||
try {
|
||||
const param = {
|
||||
Bucket: meta.objectStorageBucket,
|
||||
Bucket: this.meta.objectStorageBucket,
|
||||
Key: key,
|
||||
} as DeleteObjectCommandInput;
|
||||
|
||||
await this.s3Service.delete(meta, param);
|
||||
await this.s3Service.delete(this.meta, param);
|
||||
} catch (err: any) {
|
||||
if (err.name === 'NoSuchKey') {
|
||||
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
|
||||
|
|
|
|||
|
|
@ -5,18 +5,17 @@
|
|||
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import juice from 'juice';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { validate as validateEmail } from 'deep-email-validator';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type { UserProfilesRepository } from '@/models/_.js';
|
||||
import type { MiMeta, 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 {
|
||||
|
|
@ -26,49 +25,41 @@ export class EmailService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private loggerService: LoggerService,
|
||||
private utilityService: UtilityService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('email');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async sendEmail(to: string, subject: string, html: string, text: string) {
|
||||
const meta = await this.metaService.fetch(true);
|
||||
|
||||
if (!meta.enableEmail) return;
|
||||
if (!this.meta.enableEmail) return;
|
||||
|
||||
const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
|
||||
const emailSettingUrl = `${this.config.url}/settings/email`;
|
||||
|
||||
const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
|
||||
const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== '';
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: meta.smtpHost,
|
||||
port: meta.smtpPort,
|
||||
secure: meta.smtpSecure,
|
||||
host: this.meta.smtpHost,
|
||||
port: this.meta.smtpPort,
|
||||
secure: this.meta.smtpSecure,
|
||||
ignoreTLS: !enableAuth,
|
||||
proxy: this.config.proxySmtp,
|
||||
auth: enableAuth ? {
|
||||
user: meta.smtpUser,
|
||||
pass: meta.smtpPass,
|
||||
user: this.meta.smtpUser,
|
||||
pass: this.meta.smtpPass,
|
||||
} : undefined,
|
||||
} as any);
|
||||
|
||||
try {
|
||||
// TODO: htmlサニタイズ
|
||||
const info = await transporter.sendMail({
|
||||
from: meta.email!,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: `<!doctype html>
|
||||
const htmlContent = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
@ -133,7 +124,7 @@ export class EmailService {
|
|||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<img src="${ meta.logoImageUrl ?? meta.iconUrl ?? iconUrl }"/>
|
||||
<img src="${ this.meta.logoImageUrl ?? this.meta.iconUrl ?? iconUrl }"/>
|
||||
</header>
|
||||
<article>
|
||||
<h1>${ subject }</h1>
|
||||
|
|
@ -147,7 +138,18 @@ export class EmailService {
|
|||
<a href="${ this.config.url }">${ this.config.host }</a>
|
||||
</nav>
|
||||
</body>
|
||||
</html>`,
|
||||
</html>`;
|
||||
|
||||
const inlinedHtml = juice(htmlContent);
|
||||
|
||||
try {
|
||||
// TODO: htmlサニタイズ
|
||||
const info = await transporter.sendMail({
|
||||
from: this.meta.email!,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: inlinedHtml,
|
||||
});
|
||||
|
||||
this.logger.info(`Message sent: ${info.messageId}`);
|
||||
|
|
@ -162,8 +164,6 @@ export class EmailService {
|
|||
available: boolean;
|
||||
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
|
||||
}> {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
const exist = await this.userProfilesRepository.countBy({
|
||||
emailVerified: true,
|
||||
email: emailAddress,
|
||||
|
|
@ -181,11 +181,11 @@ export class EmailService {
|
|||
reason?: string | null,
|
||||
} = { valid: true, reason: null };
|
||||
|
||||
if (meta.enableActiveEmailValidation) {
|
||||
if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
|
||||
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
|
||||
} else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
|
||||
validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
|
||||
if (this.meta.enableActiveEmailValidation) {
|
||||
if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) {
|
||||
validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey);
|
||||
} else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) {
|
||||
validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey);
|
||||
} else {
|
||||
validated = await validateEmail({
|
||||
email: emailAddress,
|
||||
|
|
@ -215,7 +215,7 @@ export class EmailService {
|
|||
}
|
||||
|
||||
const emailDomain: string = emailAddress.split('@')[1];
|
||||
const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
|
||||
const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain);
|
||||
|
||||
if (isBanned) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(host: string): Promise<MiInstance> {
|
||||
public async fetchOrRegister(host: string): Promise<MiInstance> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
|
|
@ -70,6 +70,24 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(host: string): Promise<MiInstance | null> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const index = await this.instancesRepository.findOneBy({ host });
|
||||
|
||||
if (index == null) {
|
||||
this.federatedInstanceCache.set(host, null);
|
||||
return null;
|
||||
} else {
|
||||
this.federatedInstanceCache.set(host, index);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(id: MiInstance['id'], data: Partial<MiInstance>): Promise<void> {
|
||||
const result = await this.instancesRepository.createQueryBuilder().update()
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export class FetchInstanceMetadataService {
|
|||
|
||||
try {
|
||||
if (!force) {
|
||||
const _instance = await this.federatedInstanceService.fetch(host);
|
||||
const _instance = await this.federatedInstanceService.fetchOrRegister(host);
|
||||
const now = Date.now();
|
||||
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
|
||||
// unlock at the finally caluse
|
||||
|
|
|
|||
40
packages/backend/src/core/FlashService.ts
Normal file
40
packages/backend/src/core/FlashService.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { type FlashsRepository } from '@/models/_.js';
|
||||
|
||||
/**
|
||||
* MisskeyPlay関係のService
|
||||
*/
|
||||
@Injectable()
|
||||
export class FlashService {
|
||||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashRepository: FlashsRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 人気のあるPlay一覧を取得する.
|
||||
*/
|
||||
public async featured(opts?: { offset?: number, limit: number }) {
|
||||
const builder = this.flashRepository.createQueryBuilder('flash')
|
||||
.andWhere('flash.likedCount > 0')
|
||||
.andWhere('flash.visibility = :visibility', { visibility: 'public' })
|
||||
.addOrderBy('flash.likedCount', 'DESC')
|
||||
.addOrderBy('flash.updatedAt', 'DESC')
|
||||
.addOrderBy('flash.id', 'DESC');
|
||||
|
||||
if (opts?.offset) {
|
||||
builder.skip(opts.offset);
|
||||
}
|
||||
|
||||
builder.take(opts?.limit ?? 10);
|
||||
|
||||
return await builder.getMany();
|
||||
}
|
||||
}
|
||||
|
|
@ -241,7 +241,7 @@ export interface InternalEventTypes {
|
|||
avatarDecorationCreated: MiAvatarDecoration;
|
||||
avatarDecorationDeleted: MiAvatarDecoration;
|
||||
avatarDecorationUpdated: MiAvatarDecoration;
|
||||
metaUpdated: MiMeta;
|
||||
metaUpdated: { before?: MiMeta; after: MiMeta; };
|
||||
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||
updateUserProfile: MiUserProfile;
|
||||
|
|
|
|||
|
|
@ -10,16 +10,18 @@ import type { MiUser } from '@/models/User.js';
|
|||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiHashtag } from '@/models/Hashtag.js';
|
||||
import type { HashtagsRepository } from '@/models/_.js';
|
||||
import type { HashtagsRepository, MiMeta } from '@/models/_.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class HashtagService {
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
|
||||
|
||||
|
|
@ -29,7 +31,6 @@ export class HashtagService {
|
|||
private userEntityService: UserEntityService,
|
||||
private featuredService: FeaturedService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
}
|
||||
|
|
@ -160,10 +161,9 @@ export class HashtagService {
|
|||
|
||||
@bindThis
|
||||
public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
|
||||
const instance = await this.metaService.fetch();
|
||||
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
|
||||
const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t));
|
||||
if (hiddenTags.includes(hashtag)) return;
|
||||
if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return;
|
||||
if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return;
|
||||
|
||||
// YYYYMMDDHHmm (10分間隔)
|
||||
const now = new Date();
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||
switch (type) {
|
||||
case 'metaUpdated': {
|
||||
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
...(body.after),
|
||||
proxyAccount: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
break;
|
||||
|
|
@ -141,7 +141,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||
});
|
||||
}
|
||||
|
||||
this.globalEventService.publishInternalEvent('metaUpdated', updated);
|
||||
this.globalEventService.publishInternalEvent('metaUpdated', { before, after: updated });
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ export class MfmService {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { window } = new Window();
|
||||
const { happyDOM, window } = new Window();
|
||||
|
||||
const doc = window.document;
|
||||
|
||||
|
|
@ -406,7 +406,7 @@ export class MfmService {
|
|||
mention: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
const { username, host, acct } = node.props;
|
||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
|
||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
|
||||
a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`);
|
||||
a.className = 'u-url mention';
|
||||
a.textContent = acct;
|
||||
|
|
@ -457,6 +457,10 @@ export class MfmService {
|
|||
|
||||
appendChildren(nodes, body);
|
||||
|
||||
return new XMLSerializer().serializeToString(body);
|
||||
const serialized = new XMLSerializer().serializeToString(body);
|
||||
|
||||
happyDOM.close().catch(err => {});
|
||||
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ import * as mfm from 'mfm-js';
|
|||
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import RE2 from 're2';
|
||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiApp } from '@/models/App.js';
|
||||
import { concat } from '@/misc/prelude/array.js';
|
||||
|
|
@ -23,11 +22,8 @@ import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
|||
import type { IPoll } from '@/models/Poll.js';
|
||||
import { MiPoll } from '@/models/Poll.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import type { MiChannel } from '@/models/Channel.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
|
@ -51,7 +47,6 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
|
|
@ -60,6 +55,7 @@ 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';
|
||||
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
|
@ -151,11 +147,15 @@ type Option = {
|
|||
@Injectable()
|
||||
export class NoteCreateService implements OnApplicationShutdown {
|
||||
#shutdownController = new AbortController();
|
||||
private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
|
|
@ -210,7 +210,6 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private apRendererService: ApRendererService,
|
||||
private roleService: RoleService,
|
||||
private metaService: MetaService,
|
||||
private searchService: SearchService,
|
||||
private notesChart: NotesChart,
|
||||
private perUserNotesChart: PerUserNotesChart,
|
||||
|
|
@ -218,7 +217,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
private instanceChart: InstanceChart,
|
||||
private utilityService: UtilityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) { }
|
||||
) {
|
||||
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(user: {
|
||||
|
|
@ -251,10 +252,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
if (data.channel != null) data.visibleUsers = [];
|
||||
if (data.channel != null) data.localOnly = true;
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
if (data.visibility === 'public' && data.channel == null) {
|
||||
const sensitiveWords = meta.sensitiveWords;
|
||||
const sensitiveWords = this.meta.sensitiveWords;
|
||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
|
||||
data.visibility = 'home';
|
||||
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
|
||||
|
|
@ -262,17 +261,17 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
const hasProhibitedWords = await this.checkProhibitedWordsContain({
|
||||
const hasProhibitedWords = this.checkProhibitedWordsContain({
|
||||
cw: data.cw,
|
||||
text: data.text,
|
||||
pollChoices: data.poll?.choices,
|
||||
}, meta.prohibitedWords);
|
||||
}, this.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);
|
||||
const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host);
|
||||
|
||||
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
|
||||
data.visibility = 'home';
|
||||
|
|
@ -365,7 +364,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
// if the host is media-silenced, custom emojis are not allowed
|
||||
if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
|
||||
if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];
|
||||
|
||||
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
|
||||
|
||||
|
|
@ -506,21 +505,21 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
host: MiUser['host'];
|
||||
isBot: MiUser['isBot'];
|
||||
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
this.notesChart.update(note, true);
|
||||
if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) {
|
||||
if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) {
|
||||
this.perUserNotesChart.update(user, note, true);
|
||||
}
|
||||
|
||||
// Register host
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, true);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
|
||||
this.updateNotesCountQueue.enqueue(i.id, 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ハッシュタグ更新
|
||||
|
|
@ -853,15 +852,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
|
||||
const meta = await this.metaService.fetch();
|
||||
if (!meta.enableFanoutTimeline) return;
|
||||
if (!this.meta.enableFanoutTimeline) return;
|
||||
|
||||
const r = this.redisForTimelines.pipeline();
|
||||
|
||||
if (note.channelId) {
|
||||
this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
|
||||
|
||||
this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
|
||||
const channelFollowings = await this.channelFollowingsRepository.find({
|
||||
where: {
|
||||
|
|
@ -871,9 +869,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
});
|
||||
|
||||
for (const channelFollowing of channelFollowings) {
|
||||
this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -911,9 +909,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
if (!following.withReplies) continue;
|
||||
}
|
||||
|
||||
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -930,25 +928,25 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
if (!userListMembership.withReplies) continue;
|
||||
}
|
||||
|
||||
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
|
||||
// 自分自身のHTL
|
||||
if (note.userHost == null) {
|
||||
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
|
||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自分自身以外への返信
|
||||
if (isReply(note)) {
|
||||
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
|
||||
if (note.visibility === 'public' && note.userHost == null) {
|
||||
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
|
||||
|
|
@ -957,9 +955,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
|
||||
if (note.fileIds.length > 0) {
|
||||
this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
||||
this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r);
|
||||
}
|
||||
|
||||
if (note.visibility === 'public' && note.userHost == null) {
|
||||
|
|
@ -1018,9 +1016,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
|
||||
public checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
|
||||
if (prohibitedWords == null) {
|
||||
prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
|
||||
prohibitedWords = this.meta.prohibitedWords;
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -1036,12 +1034,23 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.#shutdownController.abort();
|
||||
private collapseNotesCount(oldValue: number, newValue: number) {
|
||||
return oldValue + newValue;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) {
|
||||
await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async dispose(): Promise<void> {
|
||||
this.#shutdownController.abort();
|
||||
await this.updateNotesCountQueue.performAllNow();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
|
||||
await this.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Brackets, In } from 'typeorm';
|
|||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||
import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
|
@ -19,9 +19,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
|
|
@ -32,6 +30,9 @@ export class NoteDeleteService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
|
@ -42,13 +43,11 @@ export class NoteDeleteService {
|
|||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private relayService: RelayService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private metaService: MetaService,
|
||||
private searchService: SearchService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private notesChart: NotesChart,
|
||||
|
|
@ -102,20 +101,20 @@ export class NoteDeleteService {
|
|||
}
|
||||
//#endregion
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
this.notesChart.update(note, false);
|
||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
this.perUserNotesChart.update(user, note, false);
|
||||
}
|
||||
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, false);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,26 +4,25 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import type { MiMeta, UsersRepository } from '@/models/_.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ProxyAccountService {
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(): Promise<MiLocalUser | null> {
|
||||
const meta = await this.metaService.fetch();
|
||||
if (meta.proxyAccountId == null) return null;
|
||||
return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser;
|
||||
if (this.meta.proxyAccountId == null) return null;
|
||||
return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import type { Config } from '@/config.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||
import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
|
||||
|
|
@ -54,13 +53,14 @@ export class PushNotificationService implements OnApplicationShutdown {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.swSubscriptionsRepository)
|
||||
private swSubscriptionsRepository: SwSubscriptionsRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
this.subscriptionsCache = new RedisKVCache<MiSwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
|
||||
lifetime: 1000 * 60 * 60 * 1, // 1h
|
||||
|
|
@ -73,14 +73,12 @@ export class PushNotificationService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
|
||||
if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return;
|
||||
|
||||
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
|
||||
push.setVapidDetails(this.config.url,
|
||||
meta.swPublicKey,
|
||||
meta.swPrivateKey);
|
||||
this.meta.swPublicKey,
|
||||
this.meta.swPrivateKey);
|
||||
|
||||
const subscriptions = await this.subscriptionsCache.fetch(userId);
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,19 @@ export class QueueService {
|
|||
repeat: { pattern: '*/5 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('bakeBufferedReactions', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkModeratorsActivity', {
|
||||
}, {
|
||||
// 毎時30分に起動
|
||||
repeat: { pattern: '30 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -452,10 +465,15 @@ export class QueueService {
|
|||
|
||||
/**
|
||||
* @see UserWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
* @see UserWebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||
public userWebhookDeliver(
|
||||
webhook: MiWebhook,
|
||||
type: typeof webhookEventTypes[number],
|
||||
content: unknown,
|
||||
opts?: { attempts?: number },
|
||||
) {
|
||||
const data: UserWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
|
|
@ -468,7 +486,7 @@ export class QueueService {
|
|||
};
|
||||
|
||||
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
attempts: opts?.attempts ?? 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
|
|
@ -479,10 +497,15 @@ export class QueueService {
|
|||
|
||||
/**
|
||||
* @see SystemWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
* @see SystemWebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
|
||||
public systemWebhookDeliver(
|
||||
webhook: MiSystemWebhook,
|
||||
type: SystemWebhookEventType,
|
||||
content: unknown,
|
||||
opts?: { attempts?: number },
|
||||
) {
|
||||
const data: SystemWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
|
|
@ -494,7 +517,7 @@ export class QueueService {
|
|||
};
|
||||
|
||||
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
attempts: opts?.attempts ?? 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
|
|
@ -21,7 +20,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
|
|
@ -30,9 +28,10 @@ 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';
|
||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
|
||||
|
||||
const FALLBACK = '\u2764';
|
||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||
|
||||
const legacies: Record<string, string> = {
|
||||
'like': '👍',
|
||||
|
|
@ -71,8 +70,8 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
|
|||
@Injectable()
|
||||
export class ReactionService {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
|
@ -87,12 +86,12 @@ export class ReactionService {
|
|||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private metaService: MetaService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private roleService: RoleService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private reactionsBufferingService: ReactionsBufferingService,
|
||||
private idService: IdService,
|
||||
private featuredService: FeaturedService,
|
||||
private globalEventService: GlobalEventService,
|
||||
|
|
@ -105,8 +104,6 @@ export class ReactionService {
|
|||
|
||||
@bindThis
|
||||
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
|
|
@ -152,7 +149,7 @@ export class ReactionService {
|
|||
}
|
||||
|
||||
// for media silenced host, custom emoji reactions are not allowed
|
||||
if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) {
|
||||
if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) {
|
||||
reaction = FALLBACK;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -174,7 +171,6 @@ export class ReactionService {
|
|||
reaction,
|
||||
};
|
||||
|
||||
// Create reaction
|
||||
try {
|
||||
await this.noteReactionsRepository.insert(record);
|
||||
} catch (e) {
|
||||
|
|
@ -198,16 +194,20 @@ export class ReactionService {
|
|||
}
|
||||
|
||||
// Increment reactions count
|
||||
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => sql,
|
||||
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
|
||||
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
|
||||
} : {}),
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
if (this.meta.enableReactionsBuffering) {
|
||||
await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache);
|
||||
} else {
|
||||
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => sql,
|
||||
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
|
||||
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
|
||||
} : {}),
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
||||
if (
|
||||
|
|
@ -227,7 +227,7 @@ export class ReactionService {
|
|||
}
|
||||
}
|
||||
|
||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
if (this.meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
this.perUserReactionsChart.update(user, note);
|
||||
}
|
||||
|
||||
|
|
@ -305,14 +305,18 @@ export class ReactionService {
|
|||
}
|
||||
|
||||
// Decrement reactions count
|
||||
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => sql,
|
||||
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
if (this.meta.enableReactionsBuffering) {
|
||||
await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction);
|
||||
} else {
|
||||
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => sql,
|
||||
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
||||
reaction: this.decodeReaction(exist.reaction).reaction,
|
||||
|
|
@ -334,8 +338,21 @@ export class ReactionService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、
|
||||
* データベース上には存在する「0個のリアクションがついている」という情報を削除する。
|
||||
* - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
|
||||
* - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
|
||||
*/
|
||||
@bindThis
|
||||
public convertLegacyReaction(reaction: string): string {
|
||||
reaction = this.decodeReaction(reaction).reaction;
|
||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||
return reaction;
|
||||
}
|
||||
|
||||
// TODO: 廃止
|
||||
/**
|
||||
* - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
|
||||
* - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
|
||||
* - データベース上には存在する「0個のリアクションがついている」という情報を削除する
|
||||
*/
|
||||
@bindThis
|
||||
public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
|
||||
|
|
@ -348,10 +365,7 @@ export class ReactionService {
|
|||
return count > 0;
|
||||
})
|
||||
.map(([reaction, count]) => {
|
||||
// unchecked indexed access
|
||||
const convertedReaction = legacies[reaction] as string | undefined;
|
||||
|
||||
const key = this.decodeReaction(convertedReaction ?? reaction).reaction;
|
||||
const key = this.convertLegacyReaction(reaction);
|
||||
|
||||
return [key, count] as const;
|
||||
})
|
||||
|
|
@ -406,11 +420,4 @@ export class ReactionService {
|
|||
host: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public convertLegacyReaction(reaction: string): string {
|
||||
reaction = this.decodeReaction(reaction).reaction;
|
||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||
return reaction;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
211
packages/backend/src/core/ReactionsBufferingService.ts
Normal file
211
packages/backend/src/core/ReactionsBufferingService.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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 { DI } from '@/di-symbols.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiUser, NotesRepository } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas';
|
||||
const REDIS_PAIR_PREFIX = 'reactionsBufferPairs';
|
||||
|
||||
@Injectable()
|
||||
export class ReactionsBufferingService implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisForReactions)
|
||||
private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string) {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'metaUpdated': {
|
||||
// リアクションバッファリングが有効→無効になったら即bake
|
||||
if (body.before != null && body.before.enableReactionsBuffering && !body.after.enableReactionsBuffering) {
|
||||
this.bake();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(noteId: MiNote['id'], userId: MiUser['id'], reaction: string, currentPairs: string[]): Promise<void> {
|
||||
const pipeline = this.redisForReactions.pipeline();
|
||||
pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, 1);
|
||||
for (let i = 0; i < currentPairs.length; i++) {
|
||||
pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, i, currentPairs[i]);
|
||||
}
|
||||
pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, Date.now(), `${userId}/${reaction}`);
|
||||
pipeline.zremrangebyrank(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -(PER_NOTE_REACTION_USER_PAIR_CACHE_MAX + 1));
|
||||
await pipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(noteId: MiNote['id'], userId: MiUser['id'], reaction: string): Promise<void> {
|
||||
const pipeline = this.redisForReactions.pipeline();
|
||||
pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, -1);
|
||||
pipeline.zrem(`${REDIS_PAIR_PREFIX}:${noteId}`, `${userId}/${reaction}`);
|
||||
// TODO: 「消した要素一覧」も持っておかないとcreateされた時に上書きされて復活する
|
||||
await pipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async get(noteId: MiNote['id']): Promise<{
|
||||
deltas: Record<string, number>;
|
||||
pairs: ([MiUser['id'], string])[];
|
||||
}> {
|
||||
const pipeline = this.redisForReactions.pipeline();
|
||||
pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
|
||||
pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
|
||||
const results = await pipeline.exec();
|
||||
|
||||
const resultDeltas = results![0][1] as Record<string, string>;
|
||||
const resultPairs = results![1][1] as string[];
|
||||
|
||||
const deltas = {} as Record<string, number>;
|
||||
for (const [name, count] of Object.entries(resultDeltas)) {
|
||||
deltas[name] = parseInt(count);
|
||||
}
|
||||
|
||||
const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
|
||||
|
||||
return {
|
||||
deltas,
|
||||
pairs,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getMany(noteIds: MiNote['id'][]): Promise<Map<MiNote['id'], {
|
||||
deltas: Record<string, number>;
|
||||
pairs: ([MiUser['id'], string])[];
|
||||
}>> {
|
||||
const map = new Map<MiNote['id'], {
|
||||
deltas: Record<string, number>;
|
||||
pairs: ([MiUser['id'], string])[];
|
||||
}>();
|
||||
|
||||
const pipeline = this.redisForReactions.pipeline();
|
||||
for (const noteId of noteIds) {
|
||||
pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
|
||||
pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
|
||||
}
|
||||
const results = await pipeline.exec();
|
||||
|
||||
const opsForEachNotes = 2;
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
const noteId = noteIds[i];
|
||||
const resultDeltas = results![i * opsForEachNotes][1] as Record<string, string>;
|
||||
const resultPairs = results![i * opsForEachNotes + 1][1] as string[];
|
||||
|
||||
const deltas = {} as Record<string, number>;
|
||||
for (const [name, count] of Object.entries(resultDeltas)) {
|
||||
deltas[name] = parseInt(count);
|
||||
}
|
||||
|
||||
const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
|
||||
|
||||
map.set(noteId, {
|
||||
deltas,
|
||||
pairs,
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
// TODO: scanは重い可能性があるので、別途 bufferedNoteIds を直接Redis上に持っておいてもいいかもしれない
|
||||
@bindThis
|
||||
public async bake(): Promise<void> {
|
||||
const bufferedNoteIds = [];
|
||||
let cursor = '0';
|
||||
do {
|
||||
// https://github.com/redis/ioredis#transparent-key-prefixing
|
||||
const result = await this.redisForReactions.scan(
|
||||
cursor,
|
||||
'MATCH',
|
||||
`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:*`,
|
||||
'COUNT',
|
||||
'1000');
|
||||
|
||||
cursor = result[0];
|
||||
bufferedNoteIds.push(...result[1].map(x => x.replace(`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:`, '')));
|
||||
} while (cursor !== '0');
|
||||
|
||||
const bufferedMap = await this.getMany(bufferedNoteIds);
|
||||
|
||||
// clear
|
||||
const pipeline = this.redisForReactions.pipeline();
|
||||
for (const noteId of bufferedNoteIds) {
|
||||
pipeline.del(`${REDIS_DELTA_PREFIX}:${noteId}`);
|
||||
pipeline.del(`${REDIS_PAIR_PREFIX}:${noteId}`);
|
||||
}
|
||||
await pipeline.exec();
|
||||
|
||||
// TODO: SQL一個にまとめたい
|
||||
for (const [noteId, buffered] of bufferedMap) {
|
||||
const sql = Object.entries(buffered.deltas)
|
||||
.map(([reaction, count]) =>
|
||||
`jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + ${count})::text::jsonb)`)
|
||||
.join(' || ');
|
||||
|
||||
this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => sql,
|
||||
reactionAndUserPairCache: buffered.pairs.map(x => x.join('/')),
|
||||
})
|
||||
.where('id = :id', { id: noteId })
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public mergeReactions(src: MiNote['reactions'], delta: Record<string, number>): MiNote['reactions'] {
|
||||
const reactions = { ...src };
|
||||
for (const [name, count] of Object.entries(delta)) {
|
||||
if (reactions[name] != null) {
|
||||
reactions[name] += count;
|
||||
} else {
|
||||
reactions[name] = count;
|
||||
}
|
||||
}
|
||||
return reactions;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
|
|||
import { In } from 'typeorm';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import type {
|
||||
MiMeta,
|
||||
MiRole,
|
||||
MiRoleAssignment,
|
||||
RoleAssignmentsRepository,
|
||||
|
|
@ -18,7 +19,6 @@ import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
|
|||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
|
@ -58,6 +58,11 @@ export type RolePolicies = {
|
|||
userEachUserListsLimit: number;
|
||||
rateLimitFactor: number;
|
||||
avatarDecorationLimit: number;
|
||||
canImportAntennas: boolean;
|
||||
canImportBlocking: boolean;
|
||||
canImportFollowing: boolean;
|
||||
canImportMuting: boolean;
|
||||
canImportUserLists: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
|
|
@ -87,10 +92,16 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
userEachUserListsLimit: 50,
|
||||
rateLimitFactor: 1,
|
||||
avatarDecorationLimit: 1,
|
||||
canImportAntennas: true,
|
||||
canImportBlocking: true,
|
||||
canImportFollowing: true,
|
||||
canImportMuting: true,
|
||||
canImportUserLists: true,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
|
||||
private rolesCache: MemorySingleCache<MiRole[]>;
|
||||
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
||||
private notificationService: NotificationService;
|
||||
|
|
@ -101,8 +112,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
|
@ -119,7 +130,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
@Inject(DI.roleAssignmentsRepository)
|
||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
|
|
@ -127,6 +137,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
private moderationLogService: ModerationLogService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
) {
|
||||
this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
|
||||
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
|
||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
|
||||
|
||||
|
|
@ -339,8 +350,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
@bindThis
|
||||
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
|
||||
const meta = await this.metaService.fetch();
|
||||
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
|
||||
const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies };
|
||||
|
||||
if (userId == null) return basePolicies;
|
||||
|
||||
|
|
@ -387,6 +397,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
|
||||
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
|
||||
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
|
||||
canImportAntennas: calc('canImportAntennas', vs => vs.some(v => v === true)),
|
||||
canImportBlocking: calc('canImportBlocking', vs => vs.some(v => v === true)),
|
||||
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
|
||||
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
|
||||
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -403,49 +418,78 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async isExplorable(role: { id: MiRole['id']} | null): Promise<boolean> {
|
||||
public async isExplorable(role: { id: MiRole['id'] } | null): Promise<boolean> {
|
||||
if (role == null) return false;
|
||||
const check = await this.rolesRepository.findOneBy({ id: role.id });
|
||||
if (check == null) return false;
|
||||
return check.isExplorable;
|
||||
}
|
||||
|
||||
/**
|
||||
* モデレーター権限のロールが割り当てられているユーザID一覧を取得する.
|
||||
*
|
||||
* @param opts.includeAdmins 管理者権限も含めるか(デフォルト: true)
|
||||
* @param opts.includeRoot rootユーザも含めるか(デフォルト: false)
|
||||
* @param opts.excludeExpire 期限切れのロールを除外するか(デフォルト: false)
|
||||
*/
|
||||
@bindThis
|
||||
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
|
||||
public async getModeratorIds(opts?: {
|
||||
includeAdmins?: boolean,
|
||||
includeRoot?: boolean,
|
||||
excludeExpire?: boolean,
|
||||
}): Promise<MiUser['id'][]> {
|
||||
const includeAdmins = opts?.includeAdmins ?? true;
|
||||
const includeRoot = opts?.includeRoot ?? false;
|
||||
const excludeExpire = opts?.excludeExpire ?? false;
|
||||
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
const moderatorRoles = includeAdmins
|
||||
? roles.filter(r => r.isModerator || r.isAdministrator)
|
||||
: roles.filter(r => r.isModerator);
|
||||
|
||||
// TODO: isRootなアカウントも含める
|
||||
const assigns = moderatorRoles.length > 0
|
||||
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
|
||||
: [];
|
||||
|
||||
// Setを経由して重複を除去(ユーザ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),
|
||||
),
|
||||
];
|
||||
const resultSet = 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));
|
||||
if (includeRoot) {
|
||||
const rootUserId = await this.rootUserIdCache.fetch(async () => {
|
||||
const it = await this.usersRepository.createQueryBuilder('users')
|
||||
.select('id')
|
||||
.where({ isRoot: true })
|
||||
.getRawOne<{ id: string }>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return it!.id;
|
||||
});
|
||||
resultSet.add(rootUserId);
|
||||
}
|
||||
|
||||
return [...resultSet].sort((x, y) => x.localeCompare(y));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getModerators(includeAdmins = true): Promise<MiUser[]> {
|
||||
const ids = await this.getModeratorIds(includeAdmins);
|
||||
const users = ids.length > 0 ? await this.usersRepository.findBy({
|
||||
id: In(ids),
|
||||
}) : [];
|
||||
return users;
|
||||
public async getModerators(opts?: {
|
||||
includeAdmins?: boolean,
|
||||
includeRoot?: boolean,
|
||||
excludeExpire?: boolean,
|
||||
}): Promise<MiUser[]> {
|
||||
const ids = await this.getModeratorIds(opts);
|
||||
return ids.length > 0
|
||||
? await this.usersRepository.findBy({
|
||||
id: In(ids),
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import bcrypt from 'bcryptjs';
|
||||
import { DataSource, IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
|
@ -20,7 +20,6 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import UsersChart from '@/core/chart/charts/users.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UserService } from '@/core/UserService.js';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -29,6 +28,9 @@ export class SignupService {
|
|||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
|
@ -39,7 +41,6 @@ export class SignupService {
|
|||
private userService: UserService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private usersChart: UsersChart,
|
||||
) {
|
||||
|
|
@ -88,8 +89,7 @@ export class SignupService {
|
|||
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
|
||||
|
||||
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
||||
const instance = await this.metaService.fetch(true);
|
||||
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
||||
const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
||||
if (isPreserved) {
|
||||
throw new Error('USED_USERNAME');
|
||||
}
|
||||
|
|
@ -150,8 +150,8 @@ export class SignupService {
|
|||
}));
|
||||
});
|
||||
|
||||
this.usersChart.update(account, true).then();
|
||||
this.userService.notifySystemWebhook(account, 'userCreated').then();
|
||||
this.usersChart.update(account, true);
|
||||
this.userService.notifySystemWebhook(account, 'userCreated');
|
||||
|
||||
return { account, secret };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
* SystemWebhook の一覧を取得する.
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchSystemWebhooks(params?: {
|
||||
public fetchSystemWebhooks(params?: {
|
||||
ids?: MiSystemWebhook['id'][];
|
||||
isActive?: MiSystemWebhook['isActive'];
|
||||
on?: MiSystemWebhook['on'];
|
||||
|
|
@ -101,8 +101,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
.log(updater, 'createSystemWebhook', {
|
||||
systemWebhookId: webhook.id,
|
||||
webhook: webhook,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
|
||||
return webhook;
|
||||
}
|
||||
|
|
@ -139,8 +138,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
systemWebhookId: beforeEntity.id,
|
||||
before: beforeEntity,
|
||||
after: afterEntity,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
|
||||
return afterEntity;
|
||||
}
|
||||
|
|
@ -158,26 +156,30 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
.log(updater, 'deleteSystemWebhook', {
|
||||
systemWebhookId: webhook.id,
|
||||
webhook,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhook をWebhook配送キューに追加する
|
||||
* @see QueueService.systemWebhookDeliver
|
||||
* // TODO: contentの型を厳格化する
|
||||
*/
|
||||
@bindThis
|
||||
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
|
||||
public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
|
||||
webhook: MiSystemWebhook | MiSystemWebhook['id'],
|
||||
type: T,
|
||||
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}`);
|
||||
this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookEntity.on.includes(type)) {
|
||||
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
|
||||
this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,23 +13,20 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
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 { 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';
|
||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
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';
|
||||
|
||||
|
|
@ -58,6 +55,9 @@ export class UserFollowingService implements OnModuleInit {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
|
@ -79,13 +79,11 @@ export class UserFollowingService implements OnModuleInit {
|
|||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private metaService: MetaService,
|
||||
private notificationService: NotificationService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private webhookService: UserWebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private instanceChart: InstanceChart,
|
||||
) {
|
||||
|
|
@ -172,7 +170,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
followee.isLocked ||
|
||||
(followeeProfile.carefulBot && follower.isBot) ||
|
||||
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
|
||||
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host))
|
||||
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host))
|
||||
) {
|
||||
let autoAccept = false;
|
||||
|
||||
|
|
@ -277,16 +275,19 @@ export class UserFollowingService implements OnModuleInit {
|
|||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
});
|
||||
|
||||
// 通知を作成
|
||||
if (follower.host === null) {
|
||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||
}, followee.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadyFollowed) return;
|
||||
|
||||
// 通知を作成
|
||||
if (follower.host === null) {
|
||||
const profile = await this.cacheService.userProfileCache.fetch(followee.id);
|
||||
|
||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||
message: profile.followedMessage,
|
||||
}, followee.id);
|
||||
}
|
||||
|
||||
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
||||
|
||||
const [followeeUser, followerUser] = await Promise.all([
|
||||
|
|
@ -304,20 +305,22 @@ export class UserFollowingService implements OnModuleInit {
|
|||
//#endregion
|
||||
|
||||
//#region Update instance stats
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowing(i.host, true);
|
||||
}
|
||||
});
|
||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, true);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowing(i.host, true);
|
||||
}
|
||||
});
|
||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
@ -436,20 +439,22 @@ export class UserFollowingService implements OnModuleInit {
|
|||
//#endregion
|
||||
|
||||
//#region Update instance stats
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowing(i.host, false);
|
||||
}
|
||||
});
|
||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowing(i.host, false);
|
||||
}
|
||||
});
|
||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
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 { type WebhooksRepository } from '@/models/_.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
|
|
@ -38,6 +38,31 @@ export class UserWebhookService implements OnApplicationShutdown {
|
|||
return this.activeWebhooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserWebhook の一覧を取得する.
|
||||
*/
|
||||
@bindThis
|
||||
public fetchWebhooks(params?: {
|
||||
ids?: MiWebhook['id'][];
|
||||
isActive?: MiWebhook['active'];
|
||||
on?: MiWebhook['on'];
|
||||
}): Promise<MiWebhook[]> {
|
||||
const query = this.webhooksRepository.createQueryBuilder('webhook');
|
||||
if (params) {
|
||||
if (params.ids && params.ids.length > 0) {
|
||||
query.andWhere('webhook.id IN (:...ids)', { ids: params.ids });
|
||||
}
|
||||
if (params.isActive !== undefined) {
|
||||
query.andWhere('webhook.active = :isActive', { isActive: params.isActive });
|
||||
}
|
||||
if (params.on && params.on.length > 0) {
|
||||
query.andWhere(':on <@ webhook.on', { on: params.on });
|
||||
}
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ import RE2 from 're2';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
|
||||
@Injectable()
|
||||
export class UtilityService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -105,4 +109,19 @@ export class UtilityService {
|
|||
if (host == null) return null;
|
||||
return toASCII(host.toLowerCase());
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isFederationAllowedHost(host: string): boolean {
|
||||
if (this.meta.federation === 'none') return false;
|
||||
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
|
||||
if (this.isBlockedHost(this.meta.blockedHosts, host)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isFederationAllowedUri(uri: string): boolean {
|
||||
const host = this.extractDbHost(uri);
|
||||
return this.isFederationAllowedHost(host);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ import {
|
|||
} from '@simplewebauthn/server';
|
||||
import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UserSecurityKeysRepository } from '@/models/_.js';
|
||||
import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { MiUser } from '@/models/_.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import type {
|
||||
|
|
@ -23,7 +22,6 @@ import type {
|
|||
AuthenticatorTransportFuture,
|
||||
CredentialDeviceType,
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialDescriptorFuture,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/types';
|
||||
|
|
@ -31,33 +29,33 @@ import type {
|
|||
@Injectable()
|
||||
export class WebAuthnService {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.userSecurityKeysRepository)
|
||||
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> {
|
||||
const instance = await this.metaService.fetch();
|
||||
public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } {
|
||||
return {
|
||||
origin: this.config.url,
|
||||
rpId: this.config.hostname,
|
||||
rpName: instance.name ?? this.config.host,
|
||||
rpIcon: instance.iconUrl ?? undefined,
|
||||
rpName: this.meta.name ?? this.config.host,
|
||||
rpIcon: this.meta.iconUrl ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
const relyingParty = this.getRelyingParty();
|
||||
const keys = await this.userSecurityKeysRepository.findBy({
|
||||
userId: userId,
|
||||
});
|
||||
|
|
@ -104,7 +102,7 @@ export class WebAuthnService {
|
|||
|
||||
await this.redisClient.del(`webauthn:challenge:${userId}`);
|
||||
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
const relyingParty = this.getRelyingParty();
|
||||
|
||||
let verification;
|
||||
try {
|
||||
|
|
@ -143,7 +141,7 @@ export class WebAuthnService {
|
|||
|
||||
@bindThis
|
||||
public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
const relyingParty = this.getRelyingParty();
|
||||
const keys = await this.userSecurityKeysRepository.findBy({
|
||||
userId: userId,
|
||||
});
|
||||
|
|
@ -166,6 +164,86 @@ export class WebAuthnService {
|
|||
return authenticationOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate Passkey Auth (Without specifying user)
|
||||
* @returns authenticationOptions
|
||||
*/
|
||||
@bindThis
|
||||
public async initiateSignInWithPasskeyAuthentication(context: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
|
||||
const authenticationOptions = await generateAuthenticationOptions({
|
||||
rpID: relyingParty.rpId,
|
||||
userVerification: 'preferred',
|
||||
});
|
||||
|
||||
await this.redisClient.setex(`webauthn:challenge:${context}`, 90, authenticationOptions.challenge);
|
||||
|
||||
return authenticationOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Webauthn AuthenticationCredential
|
||||
* @throws IdentifiableError
|
||||
* @returns If the challenge is successful, return the user ID. Otherwise, return null.
|
||||
*/
|
||||
@bindThis
|
||||
public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> {
|
||||
const challenge = await this.redisClient.get(`webauthn:challenge:${context}`);
|
||||
|
||||
if (!challenge) {
|
||||
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`);
|
||||
}
|
||||
|
||||
await this.redisClient.del(`webauthn:challenge:${context}`);
|
||||
|
||||
const key = await this.userSecurityKeysRepository.findOneBy({
|
||||
id: response.id,
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'Unknown Webauthn key');
|
||||
}
|
||||
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
|
||||
let verification;
|
||||
try {
|
||||
verification = await verifyAuthenticationResponse({
|
||||
response: response,
|
||||
expectedChallenge: challenge,
|
||||
expectedOrigin: relyingParty.origin,
|
||||
expectedRPID: relyingParty.rpId,
|
||||
authenticator: {
|
||||
credentialID: key.id,
|
||||
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
|
||||
counter: key.counter,
|
||||
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
|
||||
},
|
||||
requireUserVerification: true,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`);
|
||||
}
|
||||
|
||||
const { verified, authenticationInfo } = verification;
|
||||
|
||||
if (!verified) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.userSecurityKeysRepository.update({
|
||||
id: response.id,
|
||||
}, {
|
||||
lastUsed: new Date(),
|
||||
counter: authenticationInfo.newCounter,
|
||||
credentialDeviceType: authenticationInfo.credentialDeviceType,
|
||||
credentialBackedUp: authenticationInfo.credentialBackedUp,
|
||||
});
|
||||
|
||||
return key.userId;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
|
||||
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
|
||||
|
|
@ -209,7 +287,7 @@ export class WebAuthnService {
|
|||
}
|
||||
}
|
||||
|
||||
const relyingParty = await this.getRelyingParty();
|
||||
const relyingParty = this.getRelyingParty();
|
||||
|
||||
let verification;
|
||||
try {
|
||||
|
|
|
|||
471
packages/backend/src/core/WebhookTestService.ts
Normal file
471
packages/backend/src/core/WebhookTestService.ts
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
|
||||
const oneDayMillis = 24 * 60 * 60 * 1000;
|
||||
|
||||
type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
|
||||
targetUser: Packed<'UserLite'> | null,
|
||||
reporter: Packed<'UserLite'> | null,
|
||||
assignee: Packed<'UserLite'> | null,
|
||||
};
|
||||
|
||||
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
|
||||
const result: MiAbuseUserReport = {
|
||||
id: 'dummy-abuse-report1',
|
||||
targetUserId: 'dummy-target-user',
|
||||
targetUser: null,
|
||||
reporterId: 'dummy-reporter-user',
|
||||
reporter: null,
|
||||
assigneeId: null,
|
||||
assignee: null,
|
||||
resolved: false,
|
||||
forwarded: false,
|
||||
comment: 'This is a dummy report for testing purposes.',
|
||||
targetUserHost: null,
|
||||
reporterHost: null,
|
||||
resolvedAs: null,
|
||||
moderationNote: 'foo',
|
||||
...override,
|
||||
};
|
||||
|
||||
return {
|
||||
...result,
|
||||
targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
|
||||
reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
|
||||
assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||
return {
|
||||
id: 'dummy-user-1',
|
||||
updatedAt: new Date(Date.now() - oneDayMillis * 7),
|
||||
lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
|
||||
lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
|
||||
hideOnlineStatus: false,
|
||||
username: 'dummy1',
|
||||
usernameLower: 'dummy1',
|
||||
name: 'DummyUser1',
|
||||
followersCount: 10,
|
||||
followingCount: 5,
|
||||
movedToUri: null,
|
||||
movedAt: null,
|
||||
alsoKnownAs: null,
|
||||
notesCount: 30,
|
||||
avatarId: null,
|
||||
avatar: null,
|
||||
bannerId: null,
|
||||
banner: null,
|
||||
avatarUrl: null,
|
||||
bannerUrl: null,
|
||||
avatarBlurhash: null,
|
||||
bannerBlurhash: null,
|
||||
avatarDecorations: [],
|
||||
tags: [],
|
||||
isSuspended: false,
|
||||
isLocked: false,
|
||||
isBot: false,
|
||||
isCat: true,
|
||||
isRoot: false,
|
||||
isExplorable: true,
|
||||
isHibernated: false,
|
||||
isDeleted: false,
|
||||
requireSigninToViewContents: false,
|
||||
makeNotesFollowersOnlyBefore: null,
|
||||
makeNotesHiddenBefore: null,
|
||||
emojis: [],
|
||||
score: 0,
|
||||
host: null,
|
||||
inbox: null,
|
||||
sharedInbox: null,
|
||||
featured: null,
|
||||
uri: null,
|
||||
followersUri: null,
|
||||
token: null,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
||||
return {
|
||||
id: 'dummy-note-1',
|
||||
replyId: null,
|
||||
reply: null,
|
||||
renoteId: null,
|
||||
renote: null,
|
||||
threadId: null,
|
||||
text: 'This is a dummy note for testing purposes.',
|
||||
name: null,
|
||||
cw: null,
|
||||
userId: 'dummy-user-1',
|
||||
user: null,
|
||||
localOnly: true,
|
||||
reactionAcceptance: 'likeOnly',
|
||||
renoteCount: 10,
|
||||
repliesCount: 5,
|
||||
clippedCount: 0,
|
||||
reactions: {},
|
||||
visibility: 'public',
|
||||
uri: null,
|
||||
url: null,
|
||||
fileIds: [],
|
||||
attachedFileTypes: [],
|
||||
visibleUserIds: [],
|
||||
mentions: [],
|
||||
mentionedRemoteUsers: '[]',
|
||||
reactionAndUserPairCache: [],
|
||||
emojis: [],
|
||||
tags: [],
|
||||
hasPoll: false,
|
||||
channelId: null,
|
||||
channel: null,
|
||||
userHost: null,
|
||||
replyUserId: null,
|
||||
replyUserHost: null,
|
||||
renoteUserId: null,
|
||||
renoteUserHost: null,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
|
||||
return {
|
||||
id: note.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
userId: note.userId,
|
||||
user: toPackedUserLite(note.user ?? generateDummyUser()),
|
||||
replyId: note.replyId,
|
||||
renoteId: note.renoteId,
|
||||
isHidden: false,
|
||||
visibility: note.visibility,
|
||||
mentions: note.mentions,
|
||||
visibleUserIds: note.visibleUserIds,
|
||||
fileIds: note.fileIds,
|
||||
files: [],
|
||||
tags: note.tags,
|
||||
poll: null,
|
||||
emojis: note.emojis,
|
||||
channelId: note.channelId,
|
||||
channel: note.channel,
|
||||
localOnly: note.localOnly,
|
||||
reactionAcceptance: note.reactionAcceptance,
|
||||
reactionEmojis: {},
|
||||
reactions: {},
|
||||
reactionCount: 0,
|
||||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
uri: note.uri ?? undefined,
|
||||
url: note.url ?? undefined,
|
||||
reactionAndUserPairCache: note.reactionAndUserPairCache,
|
||||
...(detail ? {
|
||||
clippedCount: note.clippedCount,
|
||||
reply: note.reply ? toPackedNote(note.reply, false) : null,
|
||||
renote: note.renote ? toPackedNote(note.renote, true) : null,
|
||||
myReaction: null,
|
||||
} : {}),
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: user.avatarUrl,
|
||||
avatarBlurhash: user.avatarBlurhash,
|
||||
avatarDecorations: user.avatarDecorations.map(it => ({
|
||||
id: it.id,
|
||||
angle: it.angle,
|
||||
flipH: it.flipH,
|
||||
url: 'https://example.com/dummy-image001.png',
|
||||
offsetX: it.offsetX,
|
||||
offsetY: it.offsetY,
|
||||
})),
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
emojis: user.emojis,
|
||||
onlineStatus: 'active',
|
||||
badgeRoles: [],
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
|
||||
return {
|
||||
...toPackedUserLite(user),
|
||||
url: null,
|
||||
uri: null,
|
||||
movedTo: null,
|
||||
alsoKnownAs: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: user.updatedAt?.toISOString() ?? null,
|
||||
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerBlurhash: user.bannerBlurhash,
|
||||
isLocked: user.isLocked,
|
||||
isSilenced: false,
|
||||
isSuspended: user.isSuspended,
|
||||
description: null,
|
||||
location: null,
|
||||
birthday: null,
|
||||
lang: null,
|
||||
fields: [],
|
||||
verifiedLinks: [],
|
||||
followersCount: user.followersCount,
|
||||
followingCount: user.followingCount,
|
||||
notesCount: user.notesCount,
|
||||
pinnedNoteIds: [],
|
||||
pinnedNotes: [],
|
||||
pinnedPageId: null,
|
||||
pinnedPage: null,
|
||||
publicReactions: true,
|
||||
followersVisibility: 'public',
|
||||
followingVisibility: 'public',
|
||||
twoFactorEnabled: false,
|
||||
usePasswordLessLogin: false,
|
||||
securityKeys: false,
|
||||
roles: [],
|
||||
memo: null,
|
||||
moderationNote: undefined,
|
||||
isFollowing: false,
|
||||
isFollowed: false,
|
||||
hasPendingFollowRequestFromYou: false,
|
||||
hasPendingFollowRequestToYou: false,
|
||||
isBlocking: false,
|
||||
isBlocked: false,
|
||||
isMuted: false,
|
||||
isRenoteMuted: false,
|
||||
notify: 'none',
|
||||
withReplies: true,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
const dummyUser1 = generateDummyUser();
|
||||
const dummyUser2 = generateDummyUser({
|
||||
id: 'dummy-user-2',
|
||||
updatedAt: new Date(Date.now() - oneDayMillis * 30),
|
||||
lastFetchedAt: new Date(Date.now() - oneDayMillis),
|
||||
lastActiveDate: new Date(Date.now() - oneDayMillis),
|
||||
username: 'dummy2',
|
||||
usernameLower: 'dummy2',
|
||||
name: 'DummyUser2',
|
||||
followersCount: 40,
|
||||
followingCount: 50,
|
||||
notesCount: 900,
|
||||
});
|
||||
const dummyUser3 = generateDummyUser({
|
||||
id: 'dummy-user-3',
|
||||
updatedAt: new Date(Date.now() - oneDayMillis * 15),
|
||||
lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
|
||||
lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
|
||||
username: 'dummy3',
|
||||
usernameLower: 'dummy3',
|
||||
name: 'DummyUser3',
|
||||
followersCount: 60,
|
||||
followingCount: 70,
|
||||
notesCount: 15900,
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class WebhookTestService {
|
||||
public static NoSuchWebhookError = class extends Error {
|
||||
};
|
||||
|
||||
constructor(
|
||||
private userWebhookService: UserWebhookService,
|
||||
private systemWebhookService: SystemWebhookService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* UserWebhookのテスト送信を行う.
|
||||
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||
*
|
||||
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||
* - Webhookそのものの有効・無効設定(active)
|
||||
* - 送信対象イベント(on)に関する設定
|
||||
*/
|
||||
@bindThis
|
||||
public async testUserWebhook(
|
||||
params: {
|
||||
webhookId: MiWebhook['id'],
|
||||
type: WebhookEventTypes,
|
||||
override?: Partial<Omit<MiWebhook, 'id'>>,
|
||||
},
|
||||
sender: MiUser | null,
|
||||
) {
|
||||
const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
|
||||
.then(it => it.filter(it => it.userId === sender?.id));
|
||||
if (webhooks.length === 0) {
|
||||
throw new WebhookTestService.NoSuchWebhookError();
|
||||
}
|
||||
|
||||
const webhook = webhooks[0];
|
||||
const send = (contents: unknown) => {
|
||||
const merged = {
|
||||
...webhook,
|
||||
...params.override,
|
||||
};
|
||||
|
||||
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||
// また、Jobの試行回数も1回だけ.
|
||||
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||
};
|
||||
|
||||
const dummyNote1 = generateDummyNote({
|
||||
userId: dummyUser1.id,
|
||||
user: dummyUser1,
|
||||
});
|
||||
const dummyReply1 = generateDummyNote({
|
||||
id: 'dummy-reply-1',
|
||||
replyId: dummyNote1.id,
|
||||
reply: dummyNote1,
|
||||
userId: dummyUser1.id,
|
||||
user: dummyUser1,
|
||||
});
|
||||
const dummyRenote1 = generateDummyNote({
|
||||
id: 'dummy-renote-1',
|
||||
renoteId: dummyNote1.id,
|
||||
renote: dummyNote1,
|
||||
userId: dummyUser2.id,
|
||||
user: dummyUser2,
|
||||
text: null,
|
||||
});
|
||||
const dummyMention1 = generateDummyNote({
|
||||
id: 'dummy-mention-1',
|
||||
userId: dummyUser1.id,
|
||||
user: dummyUser1,
|
||||
text: `@${dummyUser2.username} This is a mention to you.`,
|
||||
mentions: [dummyUser2.id],
|
||||
});
|
||||
|
||||
switch (params.type) {
|
||||
case 'note': {
|
||||
send(toPackedNote(dummyNote1));
|
||||
break;
|
||||
}
|
||||
case 'reply': {
|
||||
send(toPackedNote(dummyReply1));
|
||||
break;
|
||||
}
|
||||
case 'renote': {
|
||||
send(toPackedNote(dummyRenote1));
|
||||
break;
|
||||
}
|
||||
case 'mention': {
|
||||
send(toPackedNote(dummyMention1));
|
||||
break;
|
||||
}
|
||||
case 'follow': {
|
||||
send(toPackedUserDetailedNotMe(dummyUser1));
|
||||
break;
|
||||
}
|
||||
case 'followed': {
|
||||
send(toPackedUserLite(dummyUser2));
|
||||
break;
|
||||
}
|
||||
case 'unfollow': {
|
||||
send(toPackedUserDetailedNotMe(dummyUser3));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhookのテスト送信を行う.
|
||||
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||
*
|
||||
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||
* - Webhookそのものの有効・無効設定(isActive)
|
||||
* - 送信対象イベント(on)に関する設定
|
||||
*/
|
||||
@bindThis
|
||||
public async testSystemWebhook(
|
||||
params: {
|
||||
webhookId: MiSystemWebhook['id'],
|
||||
type: SystemWebhookEventType,
|
||||
override?: Partial<Omit<MiSystemWebhook, 'id'>>,
|
||||
},
|
||||
) {
|
||||
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
|
||||
if (webhooks.length === 0) {
|
||||
throw new WebhookTestService.NoSuchWebhookError();
|
||||
}
|
||||
|
||||
const webhook = webhooks[0];
|
||||
const send = (contents: unknown) => {
|
||||
const merged = {
|
||||
...webhook,
|
||||
...params.override,
|
||||
};
|
||||
|
||||
// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||
// また、Jobの試行回数も1回だけ.
|
||||
this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||
};
|
||||
|
||||
switch (params.type) {
|
||||
case 'abuseReport': {
|
||||
send(generateAbuseReport({
|
||||
targetUserId: dummyUser1.id,
|
||||
targetUser: dummyUser1,
|
||||
reporterId: dummyUser2.id,
|
||||
reporter: dummyUser2,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case 'abuseReportResolved': {
|
||||
send(generateAbuseReport({
|
||||
targetUserId: dummyUser1.id,
|
||||
targetUser: dummyUser1,
|
||||
reporterId: dummyUser2.id,
|
||||
reporter: dummyUser2,
|
||||
assigneeId: dummyUser3.id,
|
||||
assignee: dummyUser3,
|
||||
resolved: true,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case 'userCreated': {
|
||||
send(toPackedUserLite(dummyUser1));
|
||||
break;
|
||||
}
|
||||
case 'inactiveModeratorsWarning': {
|
||||
const dummyTime: ModeratorInactivityRemainingTime = {
|
||||
time: 100000,
|
||||
asDays: 1,
|
||||
asHours: 24,
|
||||
};
|
||||
|
||||
send({
|
||||
remainingTime: dummyTime,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'inactiveModeratorsInvitationOnlyChanged': {
|
||||
send({});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,14 +17,13 @@ import { NoteCreateService } from '@/core/NoteCreateService.js';
|
|||
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
|
@ -48,6 +47,9 @@ export class ApInboxService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
|
@ -64,7 +66,6 @@ export class ApInboxService {
|
|||
private noteEntityService: NoteEntityService,
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private abuseReportService: AbuseReportService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private apAudienceService: ApAudienceService,
|
||||
|
|
@ -289,9 +290,8 @@ export class ApInboxService {
|
|||
return;
|
||||
}
|
||||
|
||||
// アナウンス先をブロックしてたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
|
||||
// アナウンス先が許可されているかチェック
|
||||
if (!this.utilityService.isFederationAllowedUri(uri)) return;
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
||||
|
|
|
|||
|
|
@ -494,6 +494,10 @@ export class ApRendererService {
|
|||
name: user.name,
|
||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||
_misskey_summary: profile.description,
|
||||
_misskey_followedMessage: profile.followedMessage,
|
||||
_misskey_requireSigninToViewContents: user.requireSigninToViewContents,
|
||||
_misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore,
|
||||
_misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore,
|
||||
icon: avatar ? this.renderImage(avatar) : null,
|
||||
image: banner ? this.renderImage(banner) : null,
|
||||
tag,
|
||||
|
|
|
|||
|
|
@ -205,18 +205,47 @@ export class ApRequestService {
|
|||
//#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき
|
||||
const contentType = res.headers.get('content-type');
|
||||
|
||||
if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
|
||||
if (
|
||||
res.ok &&
|
||||
(contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' &&
|
||||
_followAlternate === true
|
||||
) {
|
||||
const html = await res.text();
|
||||
const window = new Window();
|
||||
const { window, happyDOM } = new Window({
|
||||
settings: {
|
||||
disableJavaScriptEvaluation: true,
|
||||
disableJavaScriptFileLoading: true,
|
||||
disableCSSFileLoading: true,
|
||||
disableComputedStyleRendering: true,
|
||||
handleDisabledFileLoadingAsSuccess: true,
|
||||
navigation: {
|
||||
disableMainFrameNavigation: true,
|
||||
disableChildFrameNavigation: true,
|
||||
disableChildPageNavigation: true,
|
||||
disableFallbackToSetURL: true,
|
||||
},
|
||||
timer: {
|
||||
maxTimeout: 0,
|
||||
maxIntervalTime: 0,
|
||||
maxIntervalIterations: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
const document = window.document;
|
||||
document.documentElement.innerHTML = html;
|
||||
try {
|
||||
document.documentElement.innerHTML = html;
|
||||
|
||||
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
|
||||
if (alternate) {
|
||||
const href = alternate.getAttribute('href');
|
||||
if (href) {
|
||||
return await this.signedGet(href, user, false);
|
||||
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
|
||||
if (alternate) {
|
||||
const href = alternate.getAttribute('href');
|
||||
if (href) {
|
||||
return await this.signedGet(href, user, false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// something went wrong parsing the HTML, ignore the whole thing
|
||||
} finally {
|
||||
happyDOM.close().catch(err => {});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { IsNull, Not } from 'typeorm';
|
||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
|
@ -29,6 +28,7 @@ export class Resolver {
|
|||
|
||||
constructor(
|
||||
private config: Config,
|
||||
private meta: MiMeta,
|
||||
private usersRepository: UsersRepository,
|
||||
private notesRepository: NotesRepository,
|
||||
private pollsRepository: PollsRepository,
|
||||
|
|
@ -36,7 +36,6 @@ export class Resolver {
|
|||
private followRequestsRepository: FollowRequestsRepository,
|
||||
private utilityService: UtilityService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private metaService: MetaService,
|
||||
private apRequestService: ApRequestService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private apRendererService: ApRendererService,
|
||||
|
|
@ -94,8 +93,7 @@ export class Resolver {
|
|||
return await this.resolveLocal(value);
|
||||
}
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
|
||||
if (!this.utilityService.isFederationAllowedHost(host)) {
|
||||
throw new Error('Instance is blocked');
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +176,9 @@ export class ApResolverService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
|
@ -195,7 +196,6 @@ export class ApResolverService {
|
|||
|
||||
private utilityService: UtilityService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private metaService: MetaService,
|
||||
private apRequestService: ApRequestService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private apRendererService: ApRendererService,
|
||||
|
|
@ -208,6 +208,7 @@ export class ApResolverService {
|
|||
public createResolver(): Resolver {
|
||||
return new Resolver(
|
||||
this.config,
|
||||
this.meta,
|
||||
this.usersRepository,
|
||||
this.notesRepository,
|
||||
this.pollsRepository,
|
||||
|
|
@ -215,7 +216,6 @@ export class ApResolverService {
|
|||
this.followRequestsRepository,
|
||||
this.utilityService,
|
||||
this.instanceActorService,
|
||||
this.metaService,
|
||||
this.apRequestService,
|
||||
this.httpRequestService,
|
||||
this.apRendererService,
|
||||
|
|
|
|||
|
|
@ -554,6 +554,10 @@ const extension_context_definition = {
|
|||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_summary': 'misskey:_misskey_summary',
|
||||
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
|
||||
'_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents',
|
||||
'_misskey_makeNotesFollowersOnlyBefore': 'misskey:_misskey_makeNotesFollowersOnlyBefore',
|
||||
'_misskey_makeNotesHiddenBefore': 'misskey:_misskey_makeNotesHiddenBefore',
|
||||
'isCat': 'misskey:isCat',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository } from '@/models/_.js';
|
||||
import type { DriveFilesRepository, MiMeta } from '@/models/_.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { truncate } from '@/misc/truncate.js';
|
||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
|
|
@ -24,10 +23,12 @@ export class ApImageService {
|
|||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private apResolverService: ApResolverService,
|
||||
private driveService: DriveService,
|
||||
private apLoggerService: ApLoggerService,
|
||||
|
|
@ -63,12 +64,10 @@ export class ApImageService {
|
|||
|
||||
this.logger.info(`Creating the Image: ${image.url}`);
|
||||
|
||||
const instance = await this.metaService.fetch();
|
||||
|
||||
// Cache if remote file cache is on AND either
|
||||
// 1. remote sensitive file is also on
|
||||
// 2. or the image is not sensitive
|
||||
const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive);
|
||||
const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive);
|
||||
|
||||
const file = await this.driveService.uploadFromUrl({
|
||||
url: image.url,
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@
|
|||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
|
||||
import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||
import type { MiEmoji } from '@/models/Emoji.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||
|
|
@ -46,6 +45,9 @@ export class ApNoteService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
|
|
@ -65,7 +67,6 @@ export class ApNoteService {
|
|||
private apMentionService: ApMentionService,
|
||||
private apImageService: ApImageService,
|
||||
private apQuestionService: ApQuestionService,
|
||||
private metaService: MetaService,
|
||||
private appLockService: AppLockService,
|
||||
private pollService: PollService,
|
||||
private noteCreateService: NoteCreateService,
|
||||
|
|
@ -182,7 +183,7 @@ export class ApNoteService {
|
|||
/**
|
||||
* 禁止ワードチェック
|
||||
*/
|
||||
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||
const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||
if (hasProhibitedWords) {
|
||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||
}
|
||||
|
|
@ -335,9 +336,7 @@ export class ApNoteService {
|
|||
public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<MiNote | null> {
|
||||
const uri = getApId(value);
|
||||
|
||||
// ブロックしていたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
|
||||
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
||||
throw new StatusError('blocked host', 451);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit';
|
|||
import { DataSource } from 'typeorm';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
|
|
@ -35,7 +35,6 @@ import type { UtilityService } from '@/core/UtilityService.js';
|
|||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
|
|
@ -46,7 +45,7 @@ import type { ApNoteService } from './ApNoteService.js';
|
|||
import type { ApMfmService } from '../ApMfmService.js';
|
||||
import type { ApResolverService, Resolver } from '../ApResolverService.js';
|
||||
import type { ApLoggerService } from '../ApLoggerService.js';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
|
||||
import type { ApImageService } from './ApImageService.js';
|
||||
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
||||
|
||||
|
|
@ -62,7 +61,6 @@ export class ApPersonService implements OnModuleInit {
|
|||
private driveFileEntityService: DriveFileEntityService;
|
||||
private idService: IdService;
|
||||
private globalEventService: GlobalEventService;
|
||||
private metaService: MetaService;
|
||||
private federatedInstanceService: FederatedInstanceService;
|
||||
private fetchInstanceMetadataService: FetchInstanceMetadataService;
|
||||
private cacheService: CacheService;
|
||||
|
|
@ -84,6 +82,9 @@ export class ApPersonService implements OnModuleInit {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
|
|
@ -112,7 +113,6 @@ export class ApPersonService implements OnModuleInit {
|
|||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||
this.idService = this.moduleRef.get('IdService');
|
||||
this.globalEventService = this.moduleRef.get('GlobalEventService');
|
||||
this.metaService = this.moduleRef.get('MetaService');
|
||||
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
||||
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
|
||||
this.cacheService = this.moduleRef.get('CacheService');
|
||||
|
|
@ -232,6 +232,12 @@ export class ApPersonService implements OnModuleInit {
|
|||
if (user == null) throw new Error('failed to create user: user is null');
|
||||
|
||||
const [avatar, banner] = await Promise.all([icon, image].map(img => {
|
||||
// icon and image may be arrays
|
||||
// see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
|
||||
if (Array.isArray(img)) {
|
||||
img = img.find(item => item && item.url) ?? null;
|
||||
}
|
||||
|
||||
// if we have an explicitly missing image, return an
|
||||
// explicitly-null set of values
|
||||
if ((img == null) || (typeof img === 'object' && img.url == null)) {
|
||||
|
|
@ -307,8 +313,8 @@ export class ApPersonService implements OnModuleInit {
|
|||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||
}
|
||||
return 'private';
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
|
@ -356,6 +362,9 @@ export class ApPersonService implements OnModuleInit {
|
|||
tags,
|
||||
isBot,
|
||||
isCat: (person as any).isCat === true,
|
||||
requireSigninToViewContents: (person as any).requireSigninToViewContents === true,
|
||||
makeNotesFollowersOnlyBefore: (person as any).makeNotesFollowersOnlyBefore ?? null,
|
||||
makeNotesHiddenBefore: (person as any).makeNotesHiddenBefore ?? null,
|
||||
emojis,
|
||||
})) as MiRemoteUser;
|
||||
|
||||
|
|
@ -370,6 +379,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
await transactionalEntityManager.save(new MiUserProfile({
|
||||
userId: user.id,
|
||||
description: _description,
|
||||
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||
url,
|
||||
fields,
|
||||
followingVisibility,
|
||||
|
|
@ -407,13 +417,15 @@ export class ApPersonService implements OnModuleInit {
|
|||
this.cacheService.uriPersonCache.set(user.uri, user);
|
||||
|
||||
// Register host
|
||||
this.federatedInstanceService.fetch(host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.newUser(i.host);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
this.federatedInstanceService.fetchOrRegister(host).then(i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.newUser(i.host);
|
||||
}
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
});
|
||||
}
|
||||
|
||||
this.usersChart.update(user, true);
|
||||
|
||||
|
|
@ -494,8 +506,8 @@ export class ApPersonService implements OnModuleInit {
|
|||
return undefined;
|
||||
}
|
||||
return 'private';
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
|
@ -566,6 +578,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
url,
|
||||
fields,
|
||||
description: _description,
|
||||
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||
followingVisibility,
|
||||
followersVisibility,
|
||||
birthday: bday?.[0] ?? null,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ export interface IObject {
|
|||
name?: string | null;
|
||||
summary?: string;
|
||||
_misskey_summary?: string;
|
||||
_misskey_followedMessage?: string | null;
|
||||
_misskey_requireSigninToViewContents?: boolean;
|
||||
_misskey_makeNotesFollowersOnlyBefore?: number | null;
|
||||
_misskey_makeNotesHiddenBefore?: number | null;
|
||||
published?: string;
|
||||
cc?: ApObject;
|
||||
to?: ApObject;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@
|
|||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import type { FollowingsRepository, InstancesRepository } from '@/models/_.js';
|
||||
import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
|
|
@ -24,13 +23,15 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private appLockService: AppLockService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
|
|
@ -43,8 +44,6 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
}
|
||||
|
||||
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('instance.host')
|
||||
.where('instance.suspensionState != \'none\'');
|
||||
|
|
@ -65,21 +64,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followeeHost)')
|
||||
.where('following.followeeHost IS NOT NULL')
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followerHost)')
|
||||
.where('following.followerHost IS NOT NULL')
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followeeHost)')
|
||||
.where('following.followeeHost IS NOT NULL')
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
|
||||
.setParameters(pubsubSubQuery.getParameters())
|
||||
|
|
@ -88,7 +87,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
|
|
@ -96,7 +95,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
|
|||
schema: 'UserDetailedNotMe',
|
||||
}) : null,
|
||||
forwarded: report.forwarded,
|
||||
resolvedAs: report.resolvedAs,
|
||||
moderationNote: report.moderationNote,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiFlash } from '@/models/Flash.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
|
@ -20,10 +18,8 @@ export class FlashEntityService {
|
|||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
|
|
@ -34,25 +30,36 @@ export class FlashEntityService {
|
|||
src: MiFlash['id'] | MiFlash,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
packedUser?: Packed<'UserLite'>,
|
||||
likedFlashIds?: MiFlash['id'][],
|
||||
},
|
||||
): Promise<Packed<'Flash'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
// { schema: 'UserDetailed' } すると無限ループするので注意
|
||||
const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
|
||||
|
||||
let isLiked = undefined;
|
||||
if (meId) {
|
||||
isLiked = hint?.likedFlashIds
|
||||
? hint.likedFlashIds.includes(flash.id)
|
||||
: await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
|
||||
}
|
||||
|
||||
return {
|
||||
id: flash.id,
|
||||
createdAt: this.idService.parse(flash.id).date.toISOString(),
|
||||
updatedAt: flash.updatedAt.toISOString(),
|
||||
userId: flash.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
|
||||
user: user,
|
||||
title: flash.title,
|
||||
summary: flash.summary,
|
||||
script: flash.script,
|
||||
visibility: flash.visibility,
|
||||
likedCount: flash.likedCount,
|
||||
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
||||
});
|
||||
isLiked: isLiked,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -63,7 +70,19 @@ export class FlashEntityService {
|
|||
const _users = flashes.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
|
||||
const _likedFlashIds = me
|
||||
? await this.flashLikesRepository.createQueryBuilder('flashLike')
|
||||
.select('flashLike.flashId')
|
||||
.where('flashLike.userId = :userId', { userId: me.id })
|
||||
.getRawMany<{ flashLike_flashId: string }>()
|
||||
.then(likes => [...new Set(likes.map(like => like.flashLike_flashId))])
|
||||
: [];
|
||||
return Promise.all(
|
||||
flashes.map(flash => this.pack(flash, me, {
|
||||
packedUser: _userMap.get(flash.userId),
|
||||
likedFlashIds: _likedFlashIds,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,19 +3,22 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiInstance } from '@/models/Instance.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiMeta } from '@/models/_.js';
|
||||
|
||||
@Injectable()
|
||||
export class InstanceEntityService {
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
private roleService: RoleService,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
|
|
@ -27,7 +30,6 @@ export class InstanceEntityService {
|
|||
instance: MiInstance,
|
||||
me?: { id: MiUser['id']; } | null | undefined,
|
||||
): Promise<Packed<'FederationInstance'>> {
|
||||
const meta = await this.metaService.fetch();
|
||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
||||
|
||||
return {
|
||||
|
|
@ -41,7 +43,7 @@ export class InstanceEntityService {
|
|||
isNotResponding: instance.isNotResponding,
|
||||
isSuspended: instance.suspensionState !== 'none',
|
||||
suspensionState: instance.suspensionState,
|
||||
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
|
||||
isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
openRegistrations: instance.openRegistrations,
|
||||
|
|
@ -49,8 +51,8 @@ export class InstanceEntityService {
|
|||
description: instance.description,
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
|
||||
isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host),
|
||||
isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host),
|
||||
isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host),
|
||||
iconUrl: instance.iconUrl,
|
||||
faviconUrl: instance.faviconUrl,
|
||||
themeColor: instance.themeColor,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import type { AdsRepository } from '@/models/_.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
|
|
@ -24,11 +23,13 @@ export class MetaEntityService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.adsRepository)
|
||||
private adsRepository: AdsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private metaService: MetaService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
) { }
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ export class MetaEntityService {
|
|||
let instance = meta;
|
||||
|
||||
if (!instance) {
|
||||
instance = await this.metaService.fetch();
|
||||
instance = this.meta;
|
||||
}
|
||||
|
||||
const ads = await this.adsRepository.createQueryBuilder('ads')
|
||||
|
|
@ -95,6 +96,7 @@ export class MetaEntityService {
|
|||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||
enableTurnstile: instance.enableTurnstile,
|
||||
turnstileSiteKey: instance.turnstileSiteKey,
|
||||
enableTestcaptcha: instance.enableTestcaptcha,
|
||||
swPublickey: instance.swPublicKey,
|
||||
themeColor: instance.themeColor,
|
||||
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
|
||||
|
|
@ -140,7 +142,7 @@ export class MetaEntityService {
|
|||
let instance = meta;
|
||||
|
||||
if (!instance) {
|
||||
instance = await this.metaService.fetch();
|
||||
instance = this.meta;
|
||||
}
|
||||
|
||||
const packed = await this.pack(instance);
|
||||
|
|
|
|||
|
|
@ -11,29 +11,57 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DebounceLoader } from '@/misc/loader.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||
import type { ReactionService } from '../ReactionService.js';
|
||||
import type { UserEntityService } from './UserEntityService.js';
|
||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
|
||||
// is-renote.tsとよしなにリンク
|
||||
function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
|
||||
return (
|
||||
note.renote != null &&
|
||||
note.reply == null &&
|
||||
note.text == null &&
|
||||
note.cw == null &&
|
||||
(note.fileIds == null || note.fileIds.length === 0) &&
|
||||
!note.hasPoll
|
||||
);
|
||||
}
|
||||
|
||||
function getAppearNoteIds(notes: MiNote[]): Set<string> {
|
||||
const appearNoteIds = new Set<string>();
|
||||
for (const note of notes) {
|
||||
if (isPureRenote(note)) {
|
||||
appearNoteIds.add(note.renoteId);
|
||||
} else {
|
||||
appearNoteIds.add(note.id);
|
||||
}
|
||||
}
|
||||
return appearNoteIds;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class NoteEntityService implements OnModuleInit {
|
||||
private userEntityService: UserEntityService;
|
||||
private driveFileEntityService: DriveFileEntityService;
|
||||
private customEmojiService: CustomEmojiService;
|
||||
private reactionService: ReactionService;
|
||||
private reactionsBufferingService: ReactionsBufferingService;
|
||||
private idService: IdService;
|
||||
private noteLoader = new DebounceLoader(this.findNoteOrFail);
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
|
@ -59,6 +87,8 @@ export class NoteEntityService implements OnModuleInit {
|
|||
//private driveFileEntityService: DriveFileEntityService,
|
||||
//private customEmojiService: CustomEmojiService,
|
||||
//private reactionService: ReactionService,
|
||||
//private reactionsBufferingService: ReactionsBufferingService,
|
||||
//private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -67,54 +97,85 @@ export class NoteEntityService implements OnModuleInit {
|
|||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
||||
this.reactionService = this.moduleRef.get('ReactionService');
|
||||
this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
|
||||
this.idService = this.moduleRef.get('IdService');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) {
|
||||
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
|
||||
// FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある)
|
||||
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
|
||||
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
|
||||
if ((followersOnlyBefore != null)
|
||||
&& (
|
||||
(followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
|
||||
|| (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
|
||||
)
|
||||
) {
|
||||
packedNote.visibility = 'followers';
|
||||
}
|
||||
}
|
||||
|
||||
if (meId === packedNote.userId) return;
|
||||
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
let hide = false;
|
||||
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
if (packedNote.visibility === 'specified') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
|
||||
if (packedNote.user.requireSigninToViewContents && meId == null) {
|
||||
hide = true;
|
||||
}
|
||||
|
||||
if (specified) {
|
||||
hide = false;
|
||||
} else {
|
||||
if (!hide) {
|
||||
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
|
||||
if ((hiddenBefore != null)
|
||||
&& (
|
||||
(hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
|
||||
|| (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
|
||||
)
|
||||
) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
if (!hide) {
|
||||
if (packedNote.visibility === 'specified') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
const specified = packedNote.visibleUserIds!.some(id => meId === id);
|
||||
|
||||
if (!specified) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
||||
if (packedNote.visibility === 'followers') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
||||
// 自分の投稿に対するリプライ
|
||||
hide = false;
|
||||
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
||||
// 自分へのメンション
|
||||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
},
|
||||
});
|
||||
if (!hide) {
|
||||
if (packedNote.visibility === 'followers') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
||||
// 自分の投稿に対するリプライ
|
||||
hide = false;
|
||||
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
||||
// 自分へのメンション
|
||||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
},
|
||||
});
|
||||
|
||||
hide = !isFollowing;
|
||||
hide = !isFollowing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +187,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
packedNote.poll = undefined;
|
||||
packedNote.cw = null;
|
||||
packedNote.isHidden = true;
|
||||
// TODO: hiddenReason みたいなのを提供しても良さそう
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +282,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
return true;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
return note.visibleUserIds.some((id: any) => meId === id);
|
||||
return note.visibleUserIds.some(id => meId === id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -287,6 +349,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
skipHide?: boolean;
|
||||
withReactionAndUserPairCache?: boolean;
|
||||
_hint_?: {
|
||||
bufferedReactions: Map<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null;
|
||||
myReactions: Map<MiNote['id'], string | null>;
|
||||
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
||||
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
|
||||
|
|
@ -303,6 +366,15 @@ export class NoteEntityService implements OnModuleInit {
|
|||
const note = typeof src === 'object' ? src : await this.noteLoader.load(src);
|
||||
const host = note.userHost;
|
||||
|
||||
const bufferedReactions = opts._hint_?.bufferedReactions != null
|
||||
? (opts._hint_.bufferedReactions.get(note.id) ?? { deltas: {}, pairs: [] })
|
||||
: this.meta.enableReactionsBuffering
|
||||
? await this.reactionsBufferingService.get(note.id)
|
||||
: { deltas: {}, pairs: [] };
|
||||
const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {}));
|
||||
|
||||
const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/')));
|
||||
|
||||
let text = note.text;
|
||||
|
||||
if (note.name && (note.url ?? note.uri)) {
|
||||
|
|
@ -315,7 +387,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
||||
: null;
|
||||
|
||||
const reactionEmojiNames = Object.keys(note.reactions)
|
||||
const reactionEmojiNames = Object.keys(reactions)
|
||||
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
||||
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
|
|
@ -334,10 +406,10 @@ export class NoteEntityService implements OnModuleInit {
|
|||
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
|
||||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
|
||||
reactions: this.reactionService.convertLegacyReactions(note.reactions),
|
||||
reactionCount: Object.values(reactions).reduce((a, b) => a + b, 0),
|
||||
reactions: reactions,
|
||||
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
||||
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
|
||||
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? reactionAndUserPairCache : undefined,
|
||||
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
|
||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||
fileIds: note.fileIds,
|
||||
|
|
@ -376,8 +448,12 @@ export class NoteEntityService implements OnModuleInit {
|
|||
|
||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||
|
||||
...(meId && Object.keys(note.reactions).length > 0 ? {
|
||||
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
||||
...(meId && Object.keys(reactions).length > 0 ? {
|
||||
myReaction: this.populateMyReaction({
|
||||
id: note.id,
|
||||
reactions: reactions,
|
||||
reactionAndUserPairCache: reactionAndUserPairCache,
|
||||
}, meId, options?._hint_),
|
||||
} : {}),
|
||||
} : {}),
|
||||
});
|
||||
|
|
@ -400,6 +476,8 @@ export class NoteEntityService implements OnModuleInit {
|
|||
) {
|
||||
if (notes.length === 0) return [];
|
||||
|
||||
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
|
||||
|
||||
const meId = me ? me.id : null;
|
||||
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
||||
if (meId) {
|
||||
|
|
@ -409,24 +487,34 @@ export class NoteEntityService implements OnModuleInit {
|
|||
const oldId = this.idService.gen(Date.now() - 2000);
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
|
||||
const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
|
||||
if (isPureRenote(note)) {
|
||||
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.renote.id, null);
|
||||
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
|
||||
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
|
||||
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
|
||||
const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
|
||||
if (pairInBuffer) {
|
||||
myReactionsMap.set(note.renote.id, pairInBuffer[1]);
|
||||
} else {
|
||||
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
|
||||
}
|
||||
} else {
|
||||
idsNeedFetchMyReaction.add(note.renote.id);
|
||||
}
|
||||
} else {
|
||||
if (note.id < oldId) {
|
||||
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
|
||||
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.id, null);
|
||||
} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
|
||||
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
||||
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
|
||||
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
|
||||
if (pairInBuffer) {
|
||||
myReactionsMap.set(note.id, pairInBuffer[1]);
|
||||
} else {
|
||||
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
||||
}
|
||||
} else {
|
||||
idsNeedFetchMyReaction.add(note.id);
|
||||
}
|
||||
|
|
@ -461,6 +549,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||
...options,
|
||||
_hint_: {
|
||||
bufferedReactions,
|
||||
myReactions: myReactionsMap,
|
||||
packedFiles,
|
||||
packedUsers,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
|
|||
async #packInternal <T extends MiNotification | MiGroupedNotification> (
|
||||
src: T,
|
||||
meId: MiUser['id'],
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
|
||||
options: {
|
||||
checkValidNotifier?: boolean;
|
||||
},
|
||||
|
|
@ -159,9 +159,16 @@ export class NotificationEntityService implements OnModuleInit {
|
|||
...(notification.type === 'roleAssigned' ? {
|
||||
role: role,
|
||||
} : {}),
|
||||
...(notification.type === 'followRequestAccepted' ? {
|
||||
message: notification.message,
|
||||
} : {}),
|
||||
...(notification.type === 'achievementEarned' ? {
|
||||
achievement: notification.achievement,
|
||||
} : {}),
|
||||
...(notification.type === 'exportCompleted' ? {
|
||||
exportedEntity: notification.exportedEntity,
|
||||
fileId: notification.fileId,
|
||||
} : {}),
|
||||
...(notification.type === 'app' ? {
|
||||
body: notification.customBody,
|
||||
header: notification.customHeader,
|
||||
|
|
@ -229,7 +236,7 @@ export class NotificationEntityService implements OnModuleInit {
|
|||
public async pack(
|
||||
src: MiNotification | MiGroupedNotification,
|
||||
meId: MiUser['id'],
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
|
||||
options: {
|
||||
checkValidNotifier?: boolean;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -490,6 +490,9 @@ export class UserEntityService implements OnModuleInit {
|
|||
}))) : [],
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
|
||||
makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined,
|
||||
makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined,
|
||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||
name: instance.name,
|
||||
softwareName: instance.softwareName,
|
||||
|
|
@ -508,7 +511,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
name: r.name,
|
||||
iconUrl: r.iconUrl,
|
||||
displayOrder: r.displayOrder,
|
||||
}))
|
||||
})),
|
||||
) : undefined,
|
||||
|
||||
...(isDetailed ? {
|
||||
|
|
@ -545,11 +548,6 @@ export class UserEntityService implements OnModuleInit {
|
|||
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
|
||||
followersVisibility: profile!.followersVisibility,
|
||||
followingVisibility: profile!.followingVisibility,
|
||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
||||
securityKeys: profile!.twoFactorEnabled
|
||||
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
|
||||
: false,
|
||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
|
|
@ -564,9 +562,18 @@ export class UserEntityService implements OnModuleInit {
|
|||
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
|
||||
} : {}),
|
||||
|
||||
...(isDetailed && (isMe || iAmModerator) ? {
|
||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
||||
securityKeys: profile!.twoFactorEnabled
|
||||
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
|
||||
: false,
|
||||
} : {}),
|
||||
|
||||
...(isDetailed && isMe ? {
|
||||
avatarId: user.avatarId,
|
||||
bannerId: user.bannerId,
|
||||
followedMessage: profile!.followedMessage,
|
||||
isModerator: isModerator,
|
||||
isAdmin: isAdmin,
|
||||
injectFeaturedNote: profile!.injectFeaturedNote,
|
||||
|
|
@ -635,6 +642,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
isRenoteMuted: relation.isRenoteMuted,
|
||||
notify: relation.following?.notify ?? 'none',
|
||||
withReplies: relation.following?.withReplies ?? false,
|
||||
followedMessage: relation.isFollowing ? profile!.followedMessage : undefined,
|
||||
} : {}),
|
||||
} as Promiseable<Packed<S>>;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import si from 'systeminformation';
|
||||
import Xev from 'xev';
|
||||
import * as osUtils from 'os-utils';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import { MiMeta } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
|
|
@ -23,7 +24,8 @@ export class ServerStatsService implements OnApplicationShutdown {
|
|||
private intervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +34,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
|||
*/
|
||||
@bindThis
|
||||
public async start(): Promise<void> {
|
||||
if (!(await this.metaService.fetch(true)).enableServerMachineStats) return;
|
||||
if (!this.meta.enableServerMachineStats) return;
|
||||
|
||||
const log = [] as any[];
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@
|
|||
* The getter will return a .bind version of the function
|
||||
* and memoize the result against a symbol on the instance
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function bindThis(target: any, key: string, descriptor: any) {
|
||||
let fn = descriptor.value;
|
||||
const fn = descriptor.value;
|
||||
|
||||
if (typeof fn !== 'function') {
|
||||
throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`);
|
||||
|
|
@ -21,26 +22,18 @@ export function bindThis(target: any, key: string, descriptor: any) {
|
|||
configurable: true,
|
||||
get() {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (this === target.prototype || this.hasOwnProperty(key) ||
|
||||
typeof fn !== 'function') {
|
||||
if (this === target.prototype || this.hasOwnProperty(key)) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
const boundFn = fn.bind(this);
|
||||
Object.defineProperty(this, key, {
|
||||
Reflect.defineProperty(this, key, {
|
||||
value: boundFn,
|
||||
configurable: true,
|
||||
get() {
|
||||
return boundFn;
|
||||
},
|
||||
set(value) {
|
||||
fn = value;
|
||||
delete this[key];
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
return boundFn;
|
||||
},
|
||||
set(value: any) {
|
||||
fn = value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
export const DI = {
|
||||
config: Symbol('config'),
|
||||
db: Symbol('db'),
|
||||
meta: Symbol('meta'),
|
||||
meilisearch: Symbol('meilisearch'),
|
||||
redis: Symbol('redis'),
|
||||
redisForPub: Symbol('redisForPub'),
|
||||
redisForSub: Symbol('redisForSub'),
|
||||
redisForTimelines: Symbol('redisForTimelines'),
|
||||
redisForReactions: Symbol('redisForReactions'),
|
||||
|
||||
//#region Repositories
|
||||
usersRepository: Symbol('usersRepository'),
|
||||
|
|
|
|||
44
packages/backend/src/misc/collapsed-queue.ts
Normal file
44
packages/backend/src/misc/collapsed-queue.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
type Job<V> = {
|
||||
value: V;
|
||||
timer: NodeJS.Timeout;
|
||||
};
|
||||
|
||||
// TODO: redis使えるようにする
|
||||
export class CollapsedQueue<K, V> {
|
||||
private jobs: Map<K, Job<V>> = new Map();
|
||||
|
||||
constructor(
|
||||
private timeout: number,
|
||||
private collapse: (oldValue: V, newValue: V) => V,
|
||||
private perform: (key: K, value: V) => Promise<void>,
|
||||
) {}
|
||||
|
||||
enqueue(key: K, value: V) {
|
||||
if (this.jobs.has(key)) {
|
||||
const old = this.jobs.get(key)!;
|
||||
const merged = this.collapse(old.value, value);
|
||||
this.jobs.set(key, { ...old, value: merged });
|
||||
} else {
|
||||
const timer = setTimeout(() => {
|
||||
const job = this.jobs.get(key)!;
|
||||
this.jobs.delete(key);
|
||||
this.perform(key, job.value);
|
||||
}, this.timeout);
|
||||
this.jobs.set(key, { value, timer });
|
||||
}
|
||||
}
|
||||
|
||||
async performAllNow() {
|
||||
const entries = [...this.jobs.entries()];
|
||||
this.jobs.clear();
|
||||
for (const [_key, job] of entries) {
|
||||
clearTimeout(job.timer);
|
||||
}
|
||||
await Promise.allSettled(entries.map(([key, job]) => this.perform(key, job.value)));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import type { onRequestHookHandler } from 'fastify';
|
|||
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
|
||||
const index = request.url.indexOf('?');
|
||||
if (~index) {
|
||||
reply.redirect(301, request.url.slice(0, index));
|
||||
reply.redirect(request.url.slice(0, index), 301);
|
||||
}
|
||||
done();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
// NoteEntityService.isPureRenote とよしなにリンク
|
||||
|
||||
type Renote =
|
||||
MiNote & {
|
||||
renoteId: NonNullable<MiNote['renoteId']>
|
||||
|
|
|
|||
|
|
@ -144,7 +144,9 @@ export interface Schema extends OfSchema {
|
|||
readonly type?: TypeStringef;
|
||||
readonly nullable?: boolean;
|
||||
readonly optional?: boolean;
|
||||
readonly prefixItems?: ReadonlyArray<Schema>;
|
||||
readonly items?: Schema;
|
||||
readonly unevaluatedItems?: Schema | boolean;
|
||||
readonly properties?: Obj;
|
||||
readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>;
|
||||
readonly description?: string;
|
||||
|
|
@ -198,6 +200,7 @@ type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X
|
|||
//type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
|
||||
type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
|
||||
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
||||
type ArrayToTuple<X extends ReadonlyArray<Schema>> = { [K in keyof X]: SchemaType<X[K]> };
|
||||
|
||||
type ObjectSchemaTypeDef<p extends Schema> =
|
||||
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
|
||||
|
|
@ -232,6 +235,12 @@ export type SchemaTypeDef<p extends Schema> =
|
|||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
) :
|
||||
p['prefixItems'] extends ReadonlyArray<Schema> ? (
|
||||
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
|
||||
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ export class MiAbuseUserReport {
|
|||
})
|
||||
public resolved: boolean;
|
||||
|
||||
/**
|
||||
* リモートサーバーに転送したかどうか
|
||||
*/
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
@ -60,6 +63,21 @@ export class MiAbuseUserReport {
|
|||
})
|
||||
public comment: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 8192, default: '',
|
||||
})
|
||||
public moderationNote: string;
|
||||
|
||||
/**
|
||||
* accept 是認 ... 通報内容が正当であり、肯定的に対応された
|
||||
* reject 否認 ... 通報内容が正当でなく、否定的に対応された
|
||||
* null ... その他
|
||||
*/
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public resolvedAs: 'accept' | 'reject' | null;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
|
|||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
export const flashVisibility = ['public', 'private'] as const;
|
||||
export type FlashVisibility = typeof flashVisibility[number];
|
||||
|
||||
@Entity('flash')
|
||||
export class MiFlash {
|
||||
@PrimaryColumn(id())
|
||||
|
|
@ -63,5 +66,5 @@ export class MiFlash {
|
|||
@Column('varchar', {
|
||||
length: 512, default: 'public',
|
||||
})
|
||||
public visibility: 'public' | 'private';
|
||||
public visibility: FlashVisibility;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ export class MiMeta {
|
|||
})
|
||||
public prohibitedWords: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, array: true, default: '{}',
|
||||
})
|
||||
public prohibitedWordsForNameOfUser: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, array: true, default: '{}',
|
||||
})
|
||||
|
|
@ -258,6 +263,11 @@ export class MiMeta {
|
|||
})
|
||||
public turnstileSecretKey: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableTestcaptcha: boolean;
|
||||
|
||||
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
|
||||
|
||||
@Column('enum', {
|
||||
|
|
@ -519,6 +529,11 @@ export class MiMeta {
|
|||
})
|
||||
public enableChartsForFederatedInstances: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public enableStatsForFederatedInstances: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
@ -589,6 +604,11 @@ export class MiMeta {
|
|||
})
|
||||
public perUserListTimelineCacheMax: number;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableReactionsBuffering: boolean;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
|
|
@ -625,4 +645,17 @@ export class MiMeta {
|
|||
nullable: true,
|
||||
})
|
||||
public urlPreviewUserAgent: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
default: 'all',
|
||||
})
|
||||
public federation: 'all' | 'specified' | 'none';
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
array: true,
|
||||
default: '{}',
|
||||
})
|
||||
public federationHosts: string[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { userExportableEntities } from '@/types.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiNote } from './Note.js';
|
||||
import { MiAccessToken } from './AccessToken.js';
|
||||
import { MiRole } from './Role.js';
|
||||
import { MiDriveFile } from './DriveFile.js';
|
||||
|
||||
export type MiNotification = {
|
||||
type: 'note';
|
||||
|
|
@ -67,6 +69,7 @@ export type MiNotification = {
|
|||
id: string;
|
||||
createdAt: string;
|
||||
notifierId: MiUser['id'];
|
||||
message: string | null;
|
||||
} | {
|
||||
type: 'roleAssigned';
|
||||
id: string;
|
||||
|
|
@ -77,6 +80,16 @@ export type MiNotification = {
|
|||
id: string;
|
||||
createdAt: string;
|
||||
achievement: string;
|
||||
} | {
|
||||
type: 'exportCompleted';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
exportedEntity: typeof userExportableEntities[number];
|
||||
fileId: MiDriveFile['id'];
|
||||
} | {
|
||||
type: 'login';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
} | {
|
||||
type: 'app';
|
||||
id: string;
|
||||
|
|
@ -85,7 +98,7 @@ export type MiNotification = {
|
|||
/**
|
||||
* アプリ通知のbody
|
||||
*/
|
||||
customBody: string | null;
|
||||
customBody: string;
|
||||
|
||||
/**
|
||||
* アプリ通知のheader
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ export const systemWebhookEventTypes = [
|
|||
'abuseReportResolved',
|
||||
// ユーザが作成された時
|
||||
'userCreated',
|
||||
// モデレータが一定期間不在である警告
|
||||
'inactiveModeratorsWarning',
|
||||
// モデレータが一定期間不在のためシステムにより招待制へと変更された
|
||||
'inactiveModeratorsInvitationOnlyChanged',
|
||||
] as const;
|
||||
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
|
||||
|
||||
|
|
|
|||
|
|
@ -155,6 +155,11 @@ export class MiUser {
|
|||
})
|
||||
public tags: string[];
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public score: number;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether the User is suspended.',
|
||||
|
|
@ -197,6 +202,23 @@ export class MiUser {
|
|||
})
|
||||
public isHibernated: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public requireSigninToViewContents: boolean;
|
||||
|
||||
// in sec, マイナスで相対時間
|
||||
@Column('integer', {
|
||||
nullable: true,
|
||||
})
|
||||
public makeNotesFollowersOnlyBefore: number | null;
|
||||
|
||||
// in sec, マイナスで相対時間
|
||||
@Column('integer', {
|
||||
nullable: true,
|
||||
})
|
||||
public makeNotesHiddenBefore: number | null;
|
||||
|
||||
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
|
|
@ -289,5 +311,6 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr
|
|||
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
||||
export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const;
|
||||
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ export class MiUserProfile {
|
|||
})
|
||||
public description: string | null;
|
||||
|
||||
// フォローされた際のメッセージ
|
||||
@Column('varchar', {
|
||||
length: 256, nullable: true,
|
||||
})
|
||||
public followedMessage: string | null;
|
||||
|
||||
// TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { id } from './util/id.js';
|
|||
import { MiUser } from './User.js';
|
||||
|
||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
||||
export type WebhookEventTypes = typeof webhookEventTypes[number];
|
||||
|
||||
@Entity('webhook')
|
||||
export class MiWebhook {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,10 @@ export const packedMetaLiteSchema = {
|
|||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
enableTestcaptcha: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
swPublickey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { notificationTypes } from '@/types.js';
|
||||
import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
|
||||
import { notificationTypes, userExportableEntities } from '@/types.js';
|
||||
|
||||
const baseSchema = {
|
||||
type: 'object',
|
||||
|
|
@ -266,6 +267,10 @@ export const packedNotificationSchema = {
|
|||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
|
|
@ -294,6 +299,37 @@ export const packedNotificationSchema = {
|
|||
achievement: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ACHIEVEMENT_TYPES,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...baseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['exportCompleted'],
|
||||
},
|
||||
exportedEntity: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: userExportableEntities,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...baseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['login'],
|
||||
},
|
||||
},
|
||||
}, {
|
||||
|
|
@ -311,11 +347,11 @@ export const packedNotificationSchema = {
|
|||
},
|
||||
header: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
icon: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
|
|
|
|||
|
|
@ -272,6 +272,26 @@ export const packedRolePoliciesSchema = {
|
|||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canImportAntennas: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canImportBlocking: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canImportFollowing: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canImportMuting: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canImportUserLists: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -115,6 +115,18 @@ export const packedUserLiteSchema = {
|
|||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
requireSigninToViewContents: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
makeNotesFollowersOnlyBefore: {
|
||||
type: 'number',
|
||||
nullable: true, optional: true,
|
||||
},
|
||||
makeNotesHiddenBefore: {
|
||||
type: 'number',
|
||||
nullable: true, optional: true,
|
||||
},
|
||||
instance: {
|
||||
type: 'object',
|
||||
nullable: false, optional: true,
|
||||
|
|
@ -346,21 +358,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
nullable: false, optional: false,
|
||||
enum: ['public', 'followers', 'private'],
|
||||
},
|
||||
twoFactorEnabled: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
default: false,
|
||||
},
|
||||
usePasswordLessLogin: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
default: false,
|
||||
},
|
||||
securityKeys: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
default: false,
|
||||
},
|
||||
roles: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
|
|
@ -370,6 +367,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
ref: 'RoleLite',
|
||||
},
|
||||
},
|
||||
followedMessage: {
|
||||
type: 'string',
|
||||
nullable: true, optional: true,
|
||||
},
|
||||
memo: {
|
||||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
|
|
@ -378,6 +379,18 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
type: 'string',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
twoFactorEnabled: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
usePasswordLessLogin: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
securityKeys: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
//#region relations
|
||||
isFollowing: {
|
||||
type: 'boolean',
|
||||
|
|
@ -437,6 +450,10 @@ export const packedMeDetailedOnlySchema = {
|
|||
nullable: true, optional: false,
|
||||
format: 'id',
|
||||
},
|
||||
followedMessage: {
|
||||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
isModerator: {
|
||||
type: 'boolean',
|
||||
nullable: true, optional: false,
|
||||
|
|
@ -622,6 +639,21 @@ export const packedMeDetailedOnlySchema = {
|
|||
nullable: false, optional: false,
|
||||
ref: 'RolePolicies',
|
||||
},
|
||||
twoFactorEnabled: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
default: false,
|
||||
},
|
||||
usePasswordLessLogin: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
default: false,
|
||||
},
|
||||
securityKeys: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
default: false,
|
||||
},
|
||||
//#region secrets
|
||||
email: {
|
||||
type: 'string',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QueueProcessorService } from './QueueProcessorService.js';
|
||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||
|
|
@ -14,6 +15,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
|||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
||||
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
|
||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||
|
|
@ -51,6 +53,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
|||
ResyncChartsProcessorService,
|
||||
CleanChartsProcessorService,
|
||||
CheckExpiredMutingsProcessorService,
|
||||
BakeBufferedReactionsProcessorService,
|
||||
CleanProcessorService,
|
||||
DeleteDriveFilesProcessorService,
|
||||
ExportCustomEmojisProcessorService,
|
||||
|
|
@ -78,6 +81,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
|||
DeliverProcessorService,
|
||||
InboxProcessorService,
|
||||
AggregateRetentionProcessorService,
|
||||
CheckExpiredMutingsProcessorService,
|
||||
CheckModeratorsActivityProcessorService,
|
||||
QueueProcessorService,
|
||||
],
|
||||
exports: [
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||
|
|
@ -39,6 +40,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
|
|||
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
|
||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
|
||||
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
|
||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
|
|
@ -65,7 +67,7 @@ function getJobInfo(job: Bull.Job | undefined, increment = false): string {
|
|||
|
||||
// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
|
||||
const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
|
||||
const maxAttempts = job.opts ? job.opts.attempts : 0;
|
||||
const maxAttempts = job.opts.attempts ?? 0;
|
||||
|
||||
return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
|
||||
}
|
||||
|
|
@ -118,24 +120,36 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
private cleanChartsProcessorService: CleanChartsProcessorService,
|
||||
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
||||
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
||||
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
|
||||
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
|
||||
private cleanProcessorService: CleanProcessorService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger;
|
||||
|
||||
function renderError(e: Error): any {
|
||||
if (e) { // 何故かeがundefinedで来ることがある
|
||||
return {
|
||||
stack: e.stack,
|
||||
message: e.message,
|
||||
name: e.name,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
stack: '?',
|
||||
message: '?',
|
||||
name: '?',
|
||||
};
|
||||
function renderError(e?: Error) {
|
||||
// 何故かeがundefinedで来ることがある
|
||||
if (!e) return '?';
|
||||
|
||||
if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') {
|
||||
return `${e.name}: ${e.message}`;
|
||||
}
|
||||
|
||||
return {
|
||||
stack: e.stack,
|
||||
message: e.message,
|
||||
name: e.name,
|
||||
};
|
||||
}
|
||||
|
||||
function renderJob(job?: Bull.Job) {
|
||||
if (!job) return '?';
|
||||
|
||||
return {
|
||||
name: job.name || undefined,
|
||||
info: getJobInfo(job),
|
||||
failedReason: job.failedReason || undefined,
|
||||
data: job.data,
|
||||
};
|
||||
}
|
||||
|
||||
//#region system
|
||||
|
|
@ -147,6 +161,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
case 'cleanCharts': return this.cleanChartsProcessorService.process();
|
||||
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
||||
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
||||
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
|
||||
case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
|
||||
case 'clean': return this.cleanProcessorService.process();
|
||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||
}
|
||||
|
|
@ -169,15 +185,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err: Error) => {
|
||||
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
|
||||
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
}
|
||||
//#endregion
|
||||
|
|
@ -226,15 +242,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
|
||||
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
}
|
||||
//#endregion
|
||||
|
|
@ -266,15 +282,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
|
||||
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
}
|
||||
//#endregion
|
||||
|
|
@ -306,15 +322,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
|
||||
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
}
|
||||
//#endregion
|
||||
|
|
@ -346,15 +362,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, {
|
||||
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
}
|
||||
//#endregion
|
||||
|
|
@ -386,15 +402,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, {
|
||||
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
}
|
||||
//#endregion
|
||||
|
|
@ -433,15 +449,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
|
||||
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
}
|
||||
//#endregion
|
||||
|
|
@ -474,15 +490,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
|
||||
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
}
|
||||
//#endregion
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import { MiMeta } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
@Injectable()
|
||||
export class BakeBufferedReactionsProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
private reactionsBufferingService: ReactionsBufferingService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(): Promise<void> {
|
||||
if (!this.meta.enableReactionsBuffering) {
|
||||
this.logger.info('Reactions buffering is disabled. Skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info('Baking buffered reactions...');
|
||||
|
||||
await this.reactionsBufferingService.bake();
|
||||
|
||||
this.logger.succ('All buffered reactions baked.');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { MiUser, type UserProfilesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
|
||||
// モデレーターが不在と判断する日付の閾値
|
||||
const MODERATOR_INACTIVITY_LIMIT_DAYS = 7;
|
||||
// 警告通知やログ出力を行う残日数の閾値
|
||||
const MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS = 2;
|
||||
// 期限から6時間ごとに通知を行う
|
||||
const MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS = 6;
|
||||
const ONE_HOUR_MILLI_SEC = 1000 * 60 * 60;
|
||||
const ONE_DAY_MILLI_SEC = ONE_HOUR_MILLI_SEC * 24;
|
||||
|
||||
export type ModeratorInactivityEvaluationResult = {
|
||||
isModeratorsInactive: boolean;
|
||||
inactiveModerators: MiUser[];
|
||||
remainingTime: ModeratorInactivityRemainingTime;
|
||||
}
|
||||
|
||||
export type ModeratorInactivityRemainingTime = {
|
||||
time: number;
|
||||
asHours: number;
|
||||
asDays: number;
|
||||
};
|
||||
|
||||
function generateModeratorInactivityMail(remainingTime: ModeratorInactivityRemainingTime) {
|
||||
const subject = 'Moderator Inactivity Warning / モデレーター不在の通知';
|
||||
|
||||
const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
|
||||
const timeVariantJa = remainingTime.asDays === 0 ? `${remainingTime.asHours} 時間` : `${remainingTime.asDays} 日間`;
|
||||
const message = [
|
||||
'To Moderators,',
|
||||
'',
|
||||
`A moderator has been inactive for a period of time. If there are ${timeVariant} of inactivity left, it will switch to invitation only.`,
|
||||
'If you do not wish to move to invitation only, you must log into Misskey and update your last active date and time.',
|
||||
'',
|
||||
'---------------',
|
||||
'',
|
||||
'To モデレーター各位',
|
||||
'',
|
||||
`モデレーターが一定期間活動していないようです。あと${timeVariantJa}活動していない状態が続くと招待制に切り替わります。`,
|
||||
'招待制に切り替わることを望まない場合は、Misskeyにログインして最終アクティブ日時を更新してください。',
|
||||
'',
|
||||
];
|
||||
|
||||
const html = message.join('<br>');
|
||||
const text = message.join('\n');
|
||||
|
||||
return {
|
||||
subject,
|
||||
html,
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
function generateInvitationOnlyChangedMail() {
|
||||
const subject = 'Change to Invitation-Only / 招待制に変更されました';
|
||||
|
||||
const message = [
|
||||
'To Moderators,',
|
||||
'',
|
||||
`Changed to invitation only because no moderator activity was detected for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days.`,
|
||||
'To cancel the invitation only, you need to access the control panel.',
|
||||
'',
|
||||
'---------------',
|
||||
'',
|
||||
'To モデレーター各位',
|
||||
'',
|
||||
`モデレーターの活動が${MODERATOR_INACTIVITY_LIMIT_DAYS}日間検出されなかったため、招待制に変更されました。`,
|
||||
'招待制を解除するには、コントロールパネルにアクセスする必要があります。',
|
||||
'',
|
||||
];
|
||||
|
||||
const html = message.join('<br>');
|
||||
const text = message.join('\n');
|
||||
|
||||
return {
|
||||
subject,
|
||||
html,
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CheckModeratorsActivityProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
private metaService: MetaService,
|
||||
private roleService: RoleService,
|
||||
private emailService: EmailService,
|
||||
private announcementService: AnnouncementService,
|
||||
private systemWebhookService: SystemWebhookService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(): Promise<void> {
|
||||
this.logger.info('start.');
|
||||
|
||||
const meta = await this.metaService.fetch(false);
|
||||
if (!meta.disableRegistration) {
|
||||
await this.processImpl();
|
||||
} else {
|
||||
this.logger.info('is already invitation only.');
|
||||
}
|
||||
|
||||
this.logger.succ('finish.');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async processImpl() {
|
||||
const evaluateResult = await this.evaluateModeratorsInactiveDays();
|
||||
if (evaluateResult.isModeratorsInactive) {
|
||||
this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`);
|
||||
|
||||
await this.changeToInvitationOnly();
|
||||
await this.notifyChangeToInvitationOnly();
|
||||
} else {
|
||||
const remainingTime = evaluateResult.remainingTime;
|
||||
if (remainingTime.asDays <= MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS) {
|
||||
const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
|
||||
this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${timeVariant}, it will switch to invitation only.`);
|
||||
|
||||
if (remainingTime.asHours % MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS === 0) {
|
||||
// ジョブの実行頻度と同等だと通知が多すぎるため期限から6時間ごとに通知する
|
||||
// つまり、のこり2日を切ったら6時間ごとに通知が送られる
|
||||
await this.notifyInactiveModeratorsWarning(remainingTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* モデレーターが不在であるかどうかを確認する。trueの場合はモデレーターが不在である。
|
||||
* isModerator, isAdministrator, isRootのいずれかがtrueのユーザを対象に、
|
||||
* {@link MiUser.lastActiveDate}の値が実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前よりも古いユーザがいるかどうかを確認する。
|
||||
* {@link MiUser.lastActiveDate}がnullの場合は、そのユーザは確認の対象外とする。
|
||||
*
|
||||
* -----
|
||||
*
|
||||
* ### サンプルパターン
|
||||
* - 実行日時: 2022-01-30 12:00:00
|
||||
* - 判定基準: 2022-01-23 12:00:00(実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前)
|
||||
*
|
||||
* #### パターン①
|
||||
* - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
|
||||
* - モデレータB: lastActiveDate = 2022-01-23 12:00:00 ※セーフ(判定基準と同値なのでギリギリ残り0日)
|
||||
* - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
|
||||
* - モデレータD: lastActiveDate = null
|
||||
*
|
||||
* この場合、モデレータBのアクティビティのみ判定基準日よりも古くないため、モデレーターが在席と判断される。
|
||||
*
|
||||
* #### パターン②
|
||||
* - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
|
||||
* - モデレータB: lastActiveDate = 2022-01-22 12:00:00 ※アウト(残り-1日)
|
||||
* - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
|
||||
* - モデレータD: lastActiveDate = null
|
||||
*
|
||||
* この場合、モデレータA, B, Cのアクティビティは判定基準日よりも古いため、モデレーターが不在と判断される。
|
||||
*/
|
||||
@bindThis
|
||||
public async evaluateModeratorsInactiveDays(): Promise<ModeratorInactivityEvaluationResult> {
|
||||
const today = new Date();
|
||||
const inactivePeriod = new Date(today);
|
||||
inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS);
|
||||
|
||||
const moderators = await this.fetchModerators()
|
||||
.then(it => it.filter(it => it.lastActiveDate != null));
|
||||
const inactiveModerators = moderators
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
.filter(it => it.lastActiveDate!.getTime() < inactivePeriod.getTime());
|
||||
|
||||
// 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime())));
|
||||
const remainingTime = newestLastActiveDate.getTime() - inactivePeriod.getTime();
|
||||
const remainingTimeAsDays = Math.floor(remainingTime / ONE_DAY_MILLI_SEC);
|
||||
const remainingTimeAsHours = Math.floor((remainingTime / ONE_HOUR_MILLI_SEC));
|
||||
|
||||
return {
|
||||
isModeratorsInactive: inactiveModerators.length === moderators.length,
|
||||
inactiveModerators,
|
||||
remainingTime: {
|
||||
time: remainingTime,
|
||||
asHours: remainingTimeAsHours,
|
||||
asDays: remainingTimeAsDays,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async changeToInvitationOnly() {
|
||||
await this.metaService.update({ disableRegistration: true });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async notifyInactiveModeratorsWarning(remainingTime: ModeratorInactivityRemainingTime) {
|
||||
// -- モデレータへのメール送信
|
||||
|
||||
const moderators = await this.fetchModerators();
|
||||
const moderatorProfiles = await this.userProfilesRepository
|
||||
.findBy({ userId: In(moderators.map(it => it.id)) })
|
||||
.then(it => new Map(it.map(it => [it.userId, it])));
|
||||
|
||||
const mail = generateModeratorInactivityMail(remainingTime);
|
||||
for (const moderator of moderators) {
|
||||
const profile = moderatorProfiles.get(moderator.id);
|
||||
if (profile && profile.email && profile.emailVerified) {
|
||||
this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
|
||||
}
|
||||
}
|
||||
|
||||
// -- SystemWebhook
|
||||
|
||||
const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
|
||||
.then(it => it.filter(it => it.on.includes('inactiveModeratorsWarning')));
|
||||
for (const systemWebhook of systemWebhooks) {
|
||||
this.systemWebhookService.enqueueSystemWebhook(
|
||||
systemWebhook,
|
||||
'inactiveModeratorsWarning',
|
||||
{ remainingTime: remainingTime },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async notifyChangeToInvitationOnly() {
|
||||
// -- モデレータへのメールとお知らせ(個人向け)送信
|
||||
|
||||
const moderators = await this.fetchModerators();
|
||||
const moderatorProfiles = await this.userProfilesRepository
|
||||
.findBy({ userId: In(moderators.map(it => it.id)) })
|
||||
.then(it => new Map(it.map(it => [it.userId, it])));
|
||||
|
||||
const mail = generateInvitationOnlyChangedMail();
|
||||
for (const moderator of moderators) {
|
||||
this.announcementService.create({
|
||||
title: mail.subject,
|
||||
text: mail.text,
|
||||
forExistingUsers: true,
|
||||
needConfirmationToRead: true,
|
||||
userId: moderator.id,
|
||||
});
|
||||
|
||||
const profile = moderatorProfiles.get(moderator.id);
|
||||
if (profile && profile.email && profile.emailVerified) {
|
||||
this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
|
||||
}
|
||||
}
|
||||
|
||||
// -- SystemWebhook
|
||||
|
||||
const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
|
||||
.then(it => it.filter(it => it.on.includes('inactiveModeratorsInvitationOnlyChanged')));
|
||||
for (const systemWebhook of systemWebhooks) {
|
||||
this.systemWebhookService.enqueueSystemWebhook(
|
||||
systemWebhook,
|
||||
'inactiveModeratorsInvitationOnlyChanged',
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchModerators() {
|
||||
// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
|
||||
return this.roleService.getModerators({
|
||||
includeAdmins: true,
|
||||
includeRoot: true,
|
||||
excludeExpire: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import * as Bull from 'bullmq';
|
||||
import { Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { InstancesRepository } from '@/models/_.js';
|
||||
import type { InstancesRepository, MiMeta } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||
|
|
@ -31,10 +30,12 @@ export class DeliverProcessorService {
|
|||
private latest: string | null;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private utilityService: UtilityService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
||||
|
|
@ -52,9 +53,7 @@ export class DeliverProcessorService {
|
|||
public async process(job: Bull.Job<DeliverJobData>): Promise<string> {
|
||||
const { host } = new URL(job.data.to);
|
||||
|
||||
// ブロックしてたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) {
|
||||
if (!this.utilityService.isFederationAllowedUri(job.data.to)) {
|
||||
return 'skip (blocked)';
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +74,17 @@ export class DeliverProcessorService {
|
|||
try {
|
||||
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
|
||||
|
||||
// Update stats
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
this.apRequestChart.deliverSucc();
|
||||
this.federationChart.deliverd(host, true);
|
||||
|
||||
// Update instance stats
|
||||
process.nextTick(async () => {
|
||||
const i = await (this.meta.enableStatsForFederatedInstances
|
||||
? this.federatedInstanceService.fetchOrRegister(host)
|
||||
: this.federatedInstanceService.fetch(host));
|
||||
|
||||
if (i == null) return;
|
||||
|
||||
if (i.isNotResponding) {
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
isNotResponding: false,
|
||||
|
|
@ -84,19 +92,22 @@ export class DeliverProcessorService {
|
|||
});
|
||||
}
|
||||
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
this.apRequestChart.deliverSucc();
|
||||
this.federationChart.deliverd(i.host, true);
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
}
|
||||
|
||||
if (meta.enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.requestSent(i.host, true);
|
||||
}
|
||||
});
|
||||
|
||||
return 'Success';
|
||||
} catch (res) {
|
||||
// Update stats
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
this.apRequestChart.deliverFail();
|
||||
this.federationChart.deliverd(host, false);
|
||||
|
||||
// Update instance stats
|
||||
this.federatedInstanceService.fetchOrRegister(host).then(i => {
|
||||
if (!i.isNotResponding) {
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
isNotResponding: true,
|
||||
|
|
@ -117,10 +128,7 @@ export class DeliverProcessorService {
|
|||
});
|
||||
}
|
||||
|
||||
this.apRequestChart.deliverFail();
|
||||
this.federationChart.deliverd(i.host, false);
|
||||
|
||||
if (meta.enableChartsForFederatedInstances) {
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.requestSent(i.host, false);
|
||||
}
|
||||
});
|
||||
|
|
@ -130,7 +138,7 @@ export class DeliverProcessorService {
|
|||
if (!res.isRetryable) {
|
||||
// 相手が閉鎖していることを明示しているため、配送停止する
|
||||
if (job.data.isSharedInbox && res.statusCode === 410) {
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
this.federatedInstanceService.fetchOrRegister(host).then(i => {
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
suspensionState: 'goneSuspended',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type { DBExportAntennasData } from '../types.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
|
@ -35,6 +36,7 @@ export class ExportAntennasProcessorService {
|
|||
private driveService: DriveService,
|
||||
private utilityService: UtilityService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas');
|
||||
}
|
||||
|
|
@ -95,6 +97,11 @@ export class ExportAntennasProcessorService {
|
|||
const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||
this.logger.succ('Exported to: ' + driveFile.id);
|
||||
|
||||
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||
exportedEntity: 'antenna',
|
||||
fileId: driveFile.id,
|
||||
});
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
|
|||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
|
@ -30,6 +31,7 @@ export class ExportBlockingProcessorService {
|
|||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private notificationService: NotificationService,
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
|
|
@ -109,6 +111,11 @@ export class ExportBlockingProcessorService {
|
|||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
|
||||
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||
exportedEntity: 'blocking',
|
||||
fileId: driveFile.id,
|
||||
});
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
|
@ -43,6 +44,7 @@ export class ExportClipsProcessorService {
|
|||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private idService: IdService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
|
||||
}
|
||||
|
|
@ -79,6 +81,11 @@ export class ExportClipsProcessorService {
|
|||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
|
||||
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||
exportedEntity: 'clip',
|
||||
fileId: driveFile.id,
|
||||
});
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import type Logger from '@/logger.js';
|
|||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
|
@ -37,6 +38,7 @@ export class ExportCustomEmojisProcessorService {
|
|||
private driveService: DriveService,
|
||||
private downloadService: DownloadService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis');
|
||||
}
|
||||
|
|
@ -134,6 +136,12 @@ export class ExportCustomEmojisProcessorService {
|
|||
const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
|
||||
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||
exportedEntity: 'customEmoji',
|
||||
fileId: driveFile.id,
|
||||
});
|
||||
|
||||
cleanup();
|
||||
archiveCleanup();
|
||||
resolve();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import type { MiPoll } from '@/models/Poll.js';
|
|||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
|
@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
|
|||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private idService: IdService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites');
|
||||
}
|
||||
|
|
@ -123,6 +125,11 @@ export class ExportFavoritesProcessorService {
|
|||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
|
||||
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||
exportedEntity: 'favorite',
|
||||
fileId: driveFile.id,
|
||||
});
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
|
|||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import type { MiFollowing } from '@/models/Following.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
|
@ -36,6 +37,7 @@ export class ExportFollowingProcessorService {
|
|||
private utilityService: UtilityService,
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-following');
|
||||
}
|
||||
|
|
@ -113,6 +115,11 @@ export class ExportFollowingProcessorService {
|
|||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
|
||||
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||
exportedEntity: 'following',
|
||||
fileId: driveFile.id,
|
||||
});
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
|
|||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
|
@ -32,6 +33,7 @@ export class ExportMutingProcessorService {
|
|||
private utilityService: UtilityService,
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-muting');
|
||||
}
|
||||
|
|
@ -110,6 +112,11 @@ export class ExportMutingProcessorService {
|
|||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
|
||||
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||
exportedEntity: 'muting',
|
||||
fileId: driveFile.id,
|
||||
});
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { JsonArrayStream } from '@/misc/JsonArrayStream.js';
|
||||
import { FileWriterStream } from '@/misc/FileWriterStream.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
|
|
@ -112,6 +113,7 @@ export class ExportNotesProcessorService {
|
|||
private queueLoggerService: QueueLoggerService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-notes');
|
||||
}
|
||||
|
|
@ -150,6 +152,11 @@ export class ExportNotesProcessorService {
|
|||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
|
||||
this.notificationService.createNotification(user.id, 'exportCompleted', {
|
||||
exportedEntity: 'note',
|
||||
fileId: driveFile.id,
|
||||
});
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue