summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2024-07-31 11:20:31 +0000
committerGitHub <noreply@github.com>2024-07-31 11:20:31 +0000
commite98f66db51aabef925ea1a8faee6c37f67071107 (patch)
tree168fcc9219f7511bbf9bc198568406bd49fc31bb /packages/backend/src
parentfix: remove unreleased section (#14246) (diff)
parentRelease: 2024.7.0 (diff)
downloadsharkey-e98f66db51aabef925ea1a8faee6c37f67071107.tar.gz
sharkey-e98f66db51aabef925ea1a8faee6c37f67071107.tar.bz2
sharkey-e98f66db51aabef925ea1a8faee6c37f67071107.zip
Merge pull request #14233 from misskey-dev/develop
Release: 2024.7.0
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/NestLogger.ts2
-rw-r--r--packages/backend/src/boot/entry.ts2
-rw-r--r--packages/backend/src/boot/master.ts2
-rw-r--r--packages/backend/src/boot/worker.ts23
-rw-r--r--packages/backend/src/config.ts18
-rw-r--r--packages/backend/src/core/AbuseReportNotificationService.ts405
-rw-r--r--packages/backend/src/core/AbuseReportService.ts128
-rw-r--r--packages/backend/src/core/ClipService.ts4
-rw-r--r--packages/backend/src/core/CoreModule.ts56
-rw-r--r--packages/backend/src/core/DriveService.ts3
-rw-r--r--packages/backend/src/core/EmailService.ts2
-rw-r--r--packages/backend/src/core/FanoutTimelineEndpointService.ts12
-rw-r--r--packages/backend/src/core/FederatedInstanceService.ts1
-rw-r--r--packages/backend/src/core/GlobalEventService.ts32
-rw-r--r--packages/backend/src/core/LoggerService.ts4
-rw-r--r--packages/backend/src/core/MfmService.ts44
-rw-r--r--packages/backend/src/core/NoteCreateService.ts29
-rw-r--r--packages/backend/src/core/QueueModule.ts38
-rw-r--r--packages/backend/src/core/QueueService.ts63
-rw-r--r--packages/backend/src/core/ReactionService.ts17
-rw-r--r--packages/backend/src/core/RoleService.ts38
-rw-r--r--packages/backend/src/core/SignupService.ts5
-rw-r--r--packages/backend/src/core/SystemWebhookService.ts233
-rw-r--r--packages/backend/src/core/UserBlockingService.ts6
-rw-r--r--packages/backend/src/core/UserFollowingService.ts18
-rw-r--r--packages/backend/src/core/UserListService.ts2
-rw-r--r--packages/backend/src/core/UserRenoteMutingService.ts52
-rw-r--r--packages/backend/src/core/UserSearchService.ts205
-rw-r--r--packages/backend/src/core/UserService.ts24
-rw-r--r--packages/backend/src/core/UserWebhookService.ts99
-rw-r--r--packages/backend/src/core/UtilityService.ts6
-rw-r--r--packages/backend/src/core/WebhookService.ts97
-rw-r--r--packages/backend/src/core/activitypub/ApAudienceService.ts3
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts13
-rw-r--r--packages/backend/src/core/activitypub/ApMfmService.ts2
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts5
-rw-r--r--packages/backend/src/core/activitypub/models/ApMentionService.ts3
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts3
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts11
-rw-r--r--packages/backend/src/core/activitypub/models/ApQuestionService.ts7
-rw-r--r--packages/backend/src/core/activitypub/models/tag.ts3
-rw-r--r--packages/backend/src/core/chart/ChartLoggerService.ts2
-rw-r--r--packages/backend/src/core/chart/charts/federation.ts6
-rw-r--r--packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts87
-rw-r--r--packages/backend/src/core/entities/AbuseUserReportEntityService.ts3
-rw-r--r--packages/backend/src/core/entities/ClipEntityService.ts2
-rw-r--r--packages/backend/src/core/entities/DriveFileEntityService.ts7
-rw-r--r--packages/backend/src/core/entities/InstanceEntityService.ts1
-rw-r--r--packages/backend/src/core/entities/InviteCodeEntityService.ts5
-rw-r--r--packages/backend/src/core/entities/MetaEntityService.ts22
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts9
-rw-r--r--packages/backend/src/core/entities/NotificationEntityService.ts13
-rw-r--r--packages/backend/src/core/entities/PageEntityService.ts3
-rw-r--r--packages/backend/src/core/entities/SystemWebhookEntityService.ts74
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts17
-rw-r--r--packages/backend/src/di-symbols.ts2
-rw-r--r--packages/backend/src/logger.ts14
-rw-r--r--packages/backend/src/misc/is-not-null.ts8
-rw-r--r--packages/backend/src/misc/is-user-related.ts4
-rw-r--r--packages/backend/src/misc/json-schema.ts28
-rw-r--r--packages/backend/src/misc/json-value.ts8
-rw-r--r--packages/backend/src/misc/prelude/array.ts38
-rw-r--r--packages/backend/src/misc/prelude/math.ts8
-rw-r--r--packages/backend/src/misc/prelude/maybe.ts25
-rw-r--r--packages/backend/src/misc/prelude/string.ts20
-rw-r--r--packages/backend/src/misc/prelude/symbol.ts6
-rw-r--r--packages/backend/src/models/AbuseReportNotificationRecipient.ts100
-rw-r--r--packages/backend/src/models/DriveFile.ts6
-rw-r--r--packages/backend/src/models/Meta.ts5
-rw-r--r--packages/backend/src/models/RepositoryModule.ts98
-rw-r--r--packages/backend/src/models/SystemWebhook.ts100
-rw-r--r--packages/backend/src/models/_.ts36
-rw-r--r--packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts50
-rw-r--r--packages/backend/src/models/json-schema/drive-file.ts2
-rw-r--r--packages/backend/src/models/json-schema/federation-instance.ts4
-rw-r--r--packages/backend/src/models/json-schema/meta.ts6
-rw-r--r--packages/backend/src/models/json-schema/note.ts1
-rw-r--r--packages/backend/src/models/json-schema/role.ts4
-rw-r--r--packages/backend/src/models/json-schema/system-webhook.ts54
-rw-r--r--packages/backend/src/postgres.ts10
-rw-r--r--packages/backend/src/queue/QueueProcessorModule.ts6
-rw-r--r--packages/backend/src/queue/QueueProcessorService.ts502
-rw-r--r--packages/backend/src/queue/const.ts3
-rw-r--r--packages/backend/src/queue/processors/DeliverProcessorService.ts6
-rw-r--r--packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts11
-rw-r--r--packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts87
-rw-r--r--packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts (renamed from packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts)6
-rw-r--r--packages/backend/src/queue/types.ts12
-rw-r--r--packages/backend/src/server/FileServerService.ts2
-rw-r--r--packages/backend/src/server/ServerService.ts2
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts42
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts42
-rw-r--r--packages/backend/src/server/api/RateLimiterService.ts36
-rw-r--r--packages/backend/src/server/api/endpoints.ts38
-rw-r--r--packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts122
-rw-r--r--packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts44
-rw-r--r--packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts55
-rw-r--r--packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts64
-rw-r--r--packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts128
-rw-r--r--packages/backend/src/server/api/endpoints/admin/ad/update.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/list.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/show-file.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/stats.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts53
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/update.ts14
-rw-r--r--packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts85
-rw-r--r--packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts44
-rw-r--r--packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts60
-rw-r--r--packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts62
-rw-r--r--packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts91
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/clips/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/update.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/federation/instances.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/create.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/update.ts25
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-antennas.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts32
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/update.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts14
-rw-r--r--packages/backend/src/server/api/endpoints/notes/reactions/create.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/pages/update.ts23
-rw-r--r--packages/backend/src/server/api/endpoints/pinned-users.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/renote-mute/create.ts23
-rw-r--r--packages/backend/src/server/api/endpoints/renote-mute/delete.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/reactions.ts19
-rw-r--r--packages/backend/src/server/api/endpoints/users/report-abuse.ts54
-rw-r--r--packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts101
-rw-r--r--packages/backend/src/server/api/endpoints/users/search.ts108
-rw-r--r--packages/backend/src/server/api/openapi/OpenApiServerService.ts2
-rw-r--r--packages/backend/src/server/api/openapi/gen-spec.ts1
-rw-r--r--packages/backend/src/server/api/stream/Connection.ts57
-rw-r--r--packages/backend/src/server/api/stream/channel.ts13
-rw-r--r--packages/backend/src/server/api/stream/channels/admin.ts3
-rw-r--r--packages/backend/src/server/api/stream/channels/antenna.ts6
-rw-r--r--packages/backend/src/server/api/stream/channels/channel.ts6
-rw-r--r--packages/backend/src/server/api/stream/channels/drive.ts3
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts7
-rw-r--r--packages/backend/src/server/api/stream/channels/hashtag.ts7
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts11
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts21
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts9
-rw-r--r--packages/backend/src/server/api/stream/channels/main.ts3
-rw-r--r--packages/backend/src/server/api/stream/channels/queue-stats.ts10
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi-game.ts33
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi.ts3
-rw-r--r--packages/backend/src/server/api/stream/channels/role-timeline.ts6
-rw-r--r--packages/backend/src/server/api/stream/channels/server-stats.ts8
-rw-r--r--packages/backend/src/server/api/stream/channels/user-list.ts10
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts17
-rw-r--r--packages/backend/src/server/web/FeedService.ts6
-rw-r--r--packages/backend/src/server/web/boot.js12
-rw-r--r--packages/backend/src/types.ts32
159 files changed, 3949 insertions, 1065 deletions
diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts
index 80f1f7a024..d0be19664f 100644
--- a/packages/backend/src/NestLogger.ts
+++ b/packages/backend/src/NestLogger.ts
@@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common';
import Logger from '@/logger.js';
const logger = new Logger('core', 'cyan');
-const nestLogger = logger.createSubLogger('nest', 'green', false);
+const nestLogger = logger.createSubLogger('nest', 'green');
export class NestLogger implements LoggerService {
/**
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index 04c6ca9723..25375c3015 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128;
const logger = new Logger('core', 'cyan');
-const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
+const clusterLogger = logger.createSubLogger('cluster', 'orange');
const ev = new Xev();
//#region Events
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 75e1a80cd1..4bc5c799cf 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -25,7 +25,7 @@ const _dirname = dirname(_filename);
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const logger = new Logger('core', 'cyan');
-const bootLogger = logger.createSubLogger('boot', 'magenta', false);
+const bootLogger = logger.createSubLogger('boot', 'magenta');
const themeColor = chalk.hex('#86b300');
diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts
index d4a7cd56e5..5d4a15b29f 100644
--- a/packages/backend/src/boot/worker.ts
+++ b/packages/backend/src/boot/worker.ts
@@ -4,13 +4,36 @@
*/
import cluster from 'node:cluster';
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { envOption } from '@/env.js';
+import { loadConfig } from '@/config.js';
import { jobQueue, server } from './common.js';
/**
* Init worker process
*/
export async function workerMain() {
+ const config = loadConfig();
+
+ if (config.sentryForBackend) {
+ Sentry.init({
+ integrations: [
+ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
+ ],
+
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+
+ // Set sampling rate for profiling - this is relative to tracesSampleRate
+ profilesSampleRate: 1.0,
+
+ maxBreadcrumbs: 0,
+
+ ...config.sentryForBackend.options,
+ });
+ }
+
if (envOption.onlyServer) {
await server();
} else if (envOption.onlyQueue) {
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 0ac521d409..3e5a1e81cd 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -23,7 +23,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
* 設定ファイルの型
*/
type Source = {
- url: string;
+ url?: string;
port?: number;
socket?: string;
chmodSocket?: string;
@@ -31,9 +31,9 @@ type Source = {
db: {
host: string;
port: number;
- db: string;
- user: string;
- pass: string;
+ db?: string;
+ user?: string;
+ pass?: string;
disableCache?: boolean;
extra?: { [x: string]: string };
};
@@ -202,13 +202,17 @@ export function loadConfig(): Config {
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
- const url = tryCreateUrl(config.url);
+ const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
const version = meta.version;
const host = url.host;
const hostname = url.hostname;
const scheme = url.protocol.replace(/:$/, '');
const wsScheme = scheme.replace('http', 'ws');
+ const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
+ const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
+ const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
+
const externalMediaProxy = config.mediaProxy ?
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
: null;
@@ -231,7 +235,7 @@ export function loadConfig(): Config {
apiUrl: `${scheme}://${host}/api`,
authUrl: `${scheme}://${host}/auth`,
driveUrl: `${scheme}://${host}/files`,
- db: config.db,
+ db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
dbReplications: config.dbReplications,
dbSlaves: config.dbSlaves,
meilisearch: config.meilisearch,
@@ -259,7 +263,7 @@ export function loadConfig(): Config {
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles,
- signToActivityPubGet: config.signToActivityPubGet,
+ signToActivityPubGet: config.signToActivityPubGet ?? true,
mediaProxy: externalMediaProxy ?? internalMediaProxy,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
videoThumbnailGenerator: config.videoThumbnailGenerator ?
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
new file mode 100644
index 0000000000..7be5335885
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -0,0 +1,405 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';
+import { Brackets, In, IsNull, Not } from 'typeorm';
+import * as Redis from 'ioredis';
+import sanitizeHtml from 'sanitize-html';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import type {
+ AbuseReportNotificationRecipientRepository,
+ MiAbuseReportNotificationRecipient,
+ MiAbuseUserReport,
+ MiUser,
+} from '@/models/_.js';
+import { EmailService } from '@/core/EmailService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportNotificationService implements OnApplicationShutdown {
+ constructor(
+ @Inject(DI.abuseReportNotificationRecipientRepository)
+ private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ private idService: IdService,
+ private roleService: RoleService,
+ private systemWebhookService: SystemWebhookService,
+ private emailService: EmailService,
+ private metaService: MetaService,
+ private moderationLogService: ModerationLogService,
+ private globalEventService: GlobalEventService,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ /**
+ * 管理者用Redisイベントを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * 通知先ユーザは{@link getModeratorIds}の取得結果に依る.
+ *
+ * @see RoleService.getModeratorIds
+ * @see GlobalEventService.publishAdminStream
+ */
+ @bindThis
+ public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const moderatorIds = await this.roleService.getModeratorIds(true, true);
+
+ for (const moderatorId of moderatorIds) {
+ for (const abuseReport of abuseReports) {
+ this.globalEventService.publishAdminStream(
+ moderatorId,
+ 'newAbuseUserReport',
+ {
+ id: abuseReport.id,
+ targetUserId: abuseReport.targetUserId,
+ reporterId: abuseReport.reporterId,
+ comment: abuseReport.comment,
+ },
+ );
+ }
+ }
+ }
+
+ /**
+ * Mailを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * メールアドレスの送信先は以下の通り.
+ * - モデレータ権限所有者ユーザ(設定画面からメールアドレスの設定を行っているユーザに限る)
+ * - metaテーブルに設定されているメールアドレス
+ *
+ * @see EmailService.sendEmail
+ */
+ @bindThis
+ public async notifyMail(abuseReports: MiAbuseUserReport[]) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
+ .filter(it => it.isActive && it.userProfile?.emailVerified)
+ .map(it => it.userProfile?.email)
+ .filter(x => x != null),
+ );
+
+ // 送信先の鮮度を保つため、毎回取得する
+ const meta = await this.metaService.fetch(true);
+ recipientEMailAddresses.push(
+ ...(meta.email ? [meta.email] : []),
+ );
+
+ if (recipientEMailAddresses.length <= 0) {
+ return;
+ }
+
+ for (const mailAddress of recipientEMailAddresses) {
+ await Promise.all(
+ abuseReports.map(it => {
+ // TODO: 送信処理はJobQueue化したい
+ return this.emailService.sendEmail(
+ mailAddress,
+ 'New Abuse Report',
+ sanitizeHtml(it.comment),
+ sanitizeHtml(it.comment),
+ );
+ }),
+ );
+ }
+ }
+
+ /**
+ * SystemWebhookを用いて{@link abuseReports}の内容を管理者各位に通知する.
+ * ここではJobQueueへのエンキューのみを行うため、即時実行されない.
+ *
+ * @see SystemWebhookService.enqueueSystemWebhook
+ */
+ @bindThis
+ public async notifySystemWebhook(
+ abuseReports: MiAbuseUserReport[],
+ type: 'abuseReport' | 'abuseReportResolved',
+ ) {
+ if (abuseReports.length <= 0) {
+ return;
+ }
+
+ const recipientWebhookIds = await this.fetchWebhookRecipients()
+ .then(it => it
+ .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
+ .map(it => it.systemWebhookId)
+ .filter(x => x != null));
+ for (const webhookId of recipientWebhookIds) {
+ await Promise.all(
+ abuseReports.map(it => {
+ return this.systemWebhookService.enqueueSystemWebhook(
+ webhookId,
+ type,
+ it,
+ );
+ }),
+ );
+ }
+ }
+
+ /**
+ * 通報の通知先一覧を取得する.
+ *
+ * @param {Object} [params] クエリの取得条件
+ * @param {Object} [params.method] 取得する通知先の通知方法
+ * @param {Object} [opts] 動作時の詳細なオプション
+ * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+ * @param {boolean} [opts.joinUser] 通知先のユーザ情報をJOINするかどうか(default: false)
+ * @param {boolean} [opts.joinSystemWebhook] 通知先のSystemWebhook情報をJOINするかどうか(default: false)
+ * @see removeUnauthorizedRecipientUsers
+ */
+ @bindThis
+ public async fetchRecipients(
+ params?: {
+ ids?: MiAbuseReportNotificationRecipient['id'][],
+ method?: RecipientMethod[],
+ },
+ opts?: {
+ removeUnauthorized?: boolean,
+ joinUser?: boolean,
+ joinSystemWebhook?: boolean,
+ },
+ ): Promise<MiAbuseReportNotificationRecipient[]> {
+ const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient');
+
+ if (opts?.joinUser) {
+ query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id');
+ query.innerJoinAndSelect('recipient.userProfile', 'userProfile');
+ }
+
+ if (opts?.joinSystemWebhook) {
+ query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook');
+ }
+
+ if (params?.ids) {
+ query.andWhere({ id: In(params.ids) });
+ }
+
+ if (params?.method) {
+ query.andWhere(new Brackets(qb => {
+ if (params.method?.includes('email')) {
+ qb.orWhere({ method: 'email', userId: Not(IsNull()) });
+ }
+ if (params.method?.includes('webhook')) {
+ qb.orWhere({ method: 'webhook', userId: IsNull() });
+ }
+ }));
+ }
+
+ const recipients = await query.getMany();
+ if (recipients.length <= 0) {
+ return [];
+ }
+
+ // アサイン有効期限切れはイベントで拾えないので、このタイミングでチェック及び削除(オプション)
+ return (opts?.removeUnauthorized ?? true)
+ ? await this.removeUnauthorizedRecipientUsers(recipients)
+ : recipients;
+ }
+
+ /**
+ * EMailの通知先一覧を取得する.
+ * リレーション先の{@link MiUser}および{@link MiUserProfile}も同時に取得する.
+ *
+ * @param {Object} [opts]
+ * @param {boolean} [opts.removeUnauthorized] 副作用としてモデレータ権限を持たない送信先ユーザをDBから削除するかどうか(default: true)
+ * @see removeUnauthorizedRecipientUsers
+ */
+ @bindThis
+ public async fetchEMailRecipients(opts?: {
+ removeUnauthorized?: boolean
+ }): Promise<MiAbuseReportNotificationRecipient[]> {
+ return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts });
+ }
+
+ /**
+ * Webhookの通知先一覧を取得する.
+ * リレーション先の{@link MiSystemWebhook}も同時に取得する.
+ */
+ @bindThis
+ public fetchWebhookRecipients(): Promise<MiAbuseReportNotificationRecipient[]> {
+ return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true });
+ }
+
+ /**
+ * 通知先を作成する.
+ */
+ @bindThis
+ public async createRecipient(
+ params: {
+ isActive: MiAbuseReportNotificationRecipient['isActive'];
+ name: MiAbuseReportNotificationRecipient['name'];
+ method: MiAbuseReportNotificationRecipient['method'];
+ userId: MiAbuseReportNotificationRecipient['userId'];
+ systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+ },
+ updater: MiUser,
+ ): Promise<MiAbuseReportNotificationRecipient> {
+ const id = this.idService.gen();
+ await this.abuseReportNotificationRecipientRepository.insert({
+ ...params,
+ id,
+ });
+
+ const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id });
+
+ this.moderationLogService
+ .log(updater, 'createAbuseReportNotificationRecipient', {
+ recipientId: id,
+ recipient: created,
+ })
+ .then();
+
+ return created;
+ }
+
+ /**
+ * 通知先を更新する.
+ */
+ @bindThis
+ public async updateRecipient(
+ params: {
+ id: MiAbuseReportNotificationRecipient['id'];
+ isActive: MiAbuseReportNotificationRecipient['isActive'];
+ name: MiAbuseReportNotificationRecipient['name'];
+ method: MiAbuseReportNotificationRecipient['method'];
+ userId: MiAbuseReportNotificationRecipient['userId'];
+ systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId'];
+ },
+ updater: MiUser,
+ ): Promise<MiAbuseReportNotificationRecipient> {
+ const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+ await this.abuseReportNotificationRecipientRepository.update(params.id, {
+ isActive: params.isActive,
+ updatedAt: new Date(),
+ name: params.name,
+ method: params.method,
+ userId: params.userId,
+ systemWebhookId: params.systemWebhookId,
+ });
+
+ const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id });
+
+ this.moderationLogService
+ .log(updater, 'updateAbuseReportNotificationRecipient', {
+ recipientId: params.id,
+ before: beforeEntity,
+ after: afterEntity,
+ })
+ .then();
+
+ return afterEntity;
+ }
+
+ /**
+ * 通知先を削除する.
+ */
+ @bindThis
+ public async deleteRecipient(
+ id: MiAbuseReportNotificationRecipient['id'],
+ updater: MiUser,
+ ) {
+ const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id });
+
+ await this.abuseReportNotificationRecipientRepository.delete(id);
+
+ this.moderationLogService
+ .log(updater, 'deleteAbuseReportNotificationRecipient', {
+ recipientId: id,
+ recipient: entity,
+ })
+ .then();
+ }
+
+ /**
+ * モデレータ権限を持たない(*1)通知先ユーザを削除する.
+ *
+ * *1: 以下の両方を満たすものの事を言う
+ * - 通知先にユーザIDが設定されている
+ * - 付与ロールにモデレータ権限がない or アサインの有効期限が切れている
+ *
+ * @param recipients 通知先一覧の配列
+ * @returns {@lisk recipients}からモデレータ権限を持たない通知先を削除した配列
+ */
+ @bindThis
+ private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
+ const userRecipients = recipients.filter(it => it.userId !== null);
+ const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
+ if (recipientUserIds.size <= 0) {
+ // ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
+ return recipients;
+ }
+
+ // モデレータ権限の有無で通知先設定を振り分ける
+ const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
+ const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
+ const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
+ for (const recipient of userRecipients) {
+ // eslint-disable-next-line
+ if (authorizedUserIds.includes(recipient.userId!)) {
+ authorizedUserRecipients.push(recipient);
+ } else {
+ unauthorizedUserRecipients.push(recipient);
+ }
+ }
+
+ // モデレータ権限を持たない通知先をDBから削除する
+ if (unauthorizedUserRecipients.length > 0) {
+ await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id));
+ }
+ const nonUserRecipients = recipients.filter(it => it.userId === null);
+ return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id));
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise<void> {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'roleUpdated':
+ case 'roleDeleted':
+ case 'userRoleUnassigned': {
+ // 場合によってはキャッシュ更新よりも先にここが呼ばれてしまう可能性があるのでnextTickで遅延実行
+ process.nextTick(async () => {
+ const recipients = await this.abuseReportNotificationRecipientRepository.findBy({
+ userId: Not(IsNull()),
+ });
+ await this.removeUnauthorizedRecipientUsers(recipients);
+ });
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
new file mode 100644
index 0000000000..69c51509ba
--- /dev/null
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -0,0 +1,128 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { InstanceActorService } from '@/core/InstanceActorService.js';
+import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { IdService } from './IdService.js';
+
+@Injectable()
+export class AbuseReportService {
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+ private idService: IdService,
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ private queueService: QueueService,
+ private instanceActorService: InstanceActorService,
+ private apRendererService: ApRendererService,
+ private moderationLogService: ModerationLogService,
+ ) {
+ }
+
+ /**
+ * ユーザからの通報をDBに記録し、その内容を下記の手段で管理者各位に通知する.
+ * - 管理者用Redisイベント
+ * - EMail(モデレータ権限所有者ユーザ+metaテーブルに設定されているメールアドレス)
+ * - SystemWebhook
+ *
+ * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+ * @see AbuseReportNotificationService.notify
+ */
+ @bindThis
+ public async report(params: {
+ targetUserId: MiAbuseUserReport['targetUserId'],
+ targetUserHost: MiAbuseUserReport['targetUserHost'],
+ reporterId: MiAbuseUserReport['reporterId'],
+ reporterHost: MiAbuseUserReport['reporterHost'],
+ comment: string,
+ }[]) {
+ const entities = params.map(param => {
+ return {
+ id: this.idService.gen(),
+ targetUserId: param.targetUserId,
+ targetUserHost: param.targetUserHost,
+ reporterId: param.reporterId,
+ reporterHost: param.reporterHost,
+ comment: param.comment,
+ };
+ });
+
+ const reports = Array.of<MiAbuseUserReport>();
+ for (const entity of entities) {
+ const report = await this.abuseUserReportsRepository.insertOne(entity);
+ reports.push(report);
+ }
+
+ return Promise.all([
+ this.abuseReportNotificationService.notifyAdminStream(reports),
+ this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'),
+ this.abuseReportNotificationService.notifyMail(reports),
+ ]);
+ }
+
+ /**
+ * 通報を解決し、その内容を下記の手段で管理者各位に通知する.
+ * - SystemWebhook
+ *
+ * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
+ * @param operator 通報を処理したユーザ
+ * @see AbuseReportNotificationService.notify
+ */
+ @bindThis
+ public async resolve(
+ params: {
+ reportId: string;
+ forward: boolean;
+ }[],
+ operator: MiUser,
+ ) {
+ const paramsMap = new Map(params.map(it => [it.reportId, it]));
+ const reports = await this.abuseUserReportsRepository.findBy({
+ id: In(params.map(it => it.reportId)),
+ });
+
+ for (const report of reports) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const ps = paramsMap.get(report.id)!;
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ resolved: true,
+ assigneeId: operator.id,
+ forwarded: ps.forward && report.targetUserHost !== null,
+ });
+
+ if (ps.forward && report.targetUserHost != null) {
+ const actor = await this.instanceActorService.getInstanceActor();
+ const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
+
+ // eslint-disable-next-line
+ const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
+ const contextAssignedFlag = this.apRendererService.addContext(flag);
+ this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
+ }
+
+ this.moderationLogService
+ .log(operator, 'resolveAbuseReport', {
+ reportId: report.id,
+ report: report,
+ forwarded: ps.forward && report.targetUserHost !== null,
+ })
+ .then();
+ }
+
+ return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
+ .then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
+ }
+}
diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts
index 9fd1ebad87..929a9db064 100644
--- a/packages/backend/src/core/ClipService.ts
+++ b/packages/backend/src/core/ClipService.ts
@@ -41,7 +41,7 @@ export class ClipService {
const currentCount = await this.clipsRepository.countBy({
userId: me.id,
});
- if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
+ if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
throw new ClipService.TooManyClipsError();
}
@@ -102,7 +102,7 @@ export class ClipService {
const currentCount = await this.clipNotesRepository.countBy({
clipId: clip.id,
});
- if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
+ if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
throw new ClipService.TooManyClipNotesError();
}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index be80df6f1c..c9427bbeb7 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -5,6 +5,14 @@
import { Module } from '@nestjs/common';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import {
+ AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserSearchService } from '@/core/UserSearchService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -53,10 +61,11 @@ import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js';
import { UserMutingService } from './UserMutingService.js';
+import { UserRenoteMutingService } from './UserRenoteMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
import { UserAuthService } from './UserAuthService.js';
import { VideoProcessingService } from './VideoProcessingService.js';
-import { WebhookService } from './WebhookService.js';
+import { UserWebhookService } from './UserWebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
@@ -144,6 +153,8 @@ import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
+const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
+const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
@@ -193,10 +204,13 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
+const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService };
+const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
-const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
+const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
+const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -225,6 +239,7 @@ const $ChartManagementService: Provider = { provide: 'ChartManagementService', u
const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
+const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService };
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
@@ -258,6 +273,7 @@ const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', u
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
+const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -285,6 +301,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
],
providers: [
LoggerService,
+ AbuseReportService,
+ AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -334,10 +352,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService,
UserListService,
UserMutingService,
+ UserRenoteMutingService,
+ UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
- WebhookService,
+ UserWebhookService,
+ SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -366,6 +387,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
+ AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -399,6 +421,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
+ SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -422,6 +445,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
+ $AbuseReportService,
+ $AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -471,10 +496,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService,
$UserListService,
$UserMutingService,
+ $UserRenoteMutingService,
+ $UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
- $WebhookService,
+ $UserWebhookService,
+ $SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -503,6 +531,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
+ $AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -536,6 +565,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
+ $SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
@@ -560,6 +590,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
exports: [
QueueModule,
LoggerService,
+ AbuseReportService,
+ AbuseReportNotificationService,
AccountMoveService,
AccountUpdateService,
AiService,
@@ -609,10 +641,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserKeypairService,
UserListService,
UserMutingService,
+ UserRenoteMutingService,
+ UserSearchService,
UserSuspendService,
UserAuthService,
VideoProcessingService,
- WebhookService,
+ UserWebhookService,
+ SystemWebhookService,
UtilityService,
FileInfoService,
SearchService,
@@ -640,6 +675,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseUserReportEntityService,
AnnouncementEntityService,
+ AbuseReportNotificationRecipientEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
@@ -673,6 +709,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
RoleEntityService,
ReversiGameEntityService,
MetaEntityService,
+ SystemWebhookEntityService,
ApAudienceService,
ApDbResolverService,
@@ -696,6 +733,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
+ $AbuseReportService,
+ $AbuseReportNotificationService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
@@ -745,10 +784,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserKeypairService,
$UserListService,
$UserMutingService,
+ $UserRenoteMutingService,
+ $UserSearchService,
$UserSuspendService,
$UserAuthService,
$VideoProcessingService,
- $WebhookService,
+ $UserWebhookService,
+ $SystemWebhookService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -776,6 +818,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AbuseUserReportEntityService,
$AnnouncementEntityService,
+ $AbuseReportNotificationRecipientEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
@@ -809,6 +852,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$RoleEntityService,
$ReversiGameEntityService,
$MetaEntityService,
+ $SystemWebhookEntityService,
$ApAudienceService,
$ApDbResolverService,
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index 37c5d1adf7..8aa04b4da7 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -43,6 +43,7 @@ import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { UtilityService } from '@/core/UtilityService.js';
type AddFileArgs = {
/** User who wish to add file */
@@ -127,6 +128,7 @@ export class DriveService {
private driveChart: DriveChart,
private perUserDriveChart: PerUserDriveChart,
private instanceChart: InstanceChart,
+ private utilityService: UtilityService,
) {
const logger = new Logger('drive', 'blue');
this.registerLogger = logger.createSubLogger('register', 'yellow');
@@ -587,6 +589,7 @@ export class DriveService {
sensitive ?? false
: false;
+ if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true;
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
if (userRoleNSFW) file.isSensitive = true;
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 08f8f80a6e..435dbbae28 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -16,6 +16,7 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
+import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class EmailService {
@@ -32,6 +33,7 @@ export class EmailService {
private loggerService: LoggerService,
private utilityService: UtilityService,
private httpRequestService: HttpRequestService,
+ private queueService: QueueService,
) {
this.logger = this.loggerService.getLogger('email');
}
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index d5058f37c2..b05af99c5e 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -55,9 +55,6 @@ export class FanoutTimelineEndpointService {
@bindThis
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
- let noteIds: string[];
- let shouldFallbackToDb = false;
-
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
@@ -67,12 +64,11 @@ export class FanoutTimelineEndpointService {
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
- const redisResultIds = Array.from(new Set(redisResult.flat(1)));
-
- redisResultIds.sort(idCompare);
- noteIds = redisResultIds.slice(0, ps.limit);
+ const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
- shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
+ let noteIds = redisResultIds.slice(0, ps.limit);
+ const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
+ const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
if (!shouldFallbackToDb) {
let filter = ps.noteFilter ?? (_note => true);
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index 6799f2c5bb..7aeeb78178 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -40,6 +40,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
+ notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
};
},
});
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 90efd63f3a..87aa70713e 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js';
+import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
import type { MiMeta } from '@/models/Meta.js';
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -208,6 +209,10 @@ type SerializedAll<T> = {
[K in keyof T]: Serialized<T[K]>;
};
+type UndefinedAsNullAll<T> = {
+ [K in keyof T]: T[K] extends undefined ? null : T[K];
+}
+
export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
@@ -227,6 +232,9 @@ export interface InternalEventTypes {
webhookCreated: MiWebhook;
webhookDeleted: MiWebhook;
webhookUpdated: MiWebhook;
+ systemWebhookCreated: MiSystemWebhook;
+ systemWebhookDeleted: MiSystemWebhook;
+ systemWebhookUpdated: MiSystemWebhook;
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
@@ -243,43 +251,45 @@ export interface InternalEventTypes {
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
}
+type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
+
// name/messages(spec) pairs dictionary
export type GlobalEvents = {
internal: {
name: 'internal';
- payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
+ payload: EventTypesToEventPayload<InternalEventTypes>;
};
broadcast: {
name: 'broadcast';
- payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
+ payload: EventTypesToEventPayload<BroadcastTypes>;
};
main: {
name: `mainStream:${MiUser['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
+ payload: EventTypesToEventPayload<MainEventTypes>;
};
drive: {
name: `driveStream:${MiUser['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
+ payload: EventTypesToEventPayload<DriveEventTypes>;
};
note: {
name: `noteStream:${MiNote['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
+ payload: EventTypesToEventPayload<NoteStreamEventTypes>;
};
userList: {
name: `userListStream:${MiUserList['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
+ payload: EventTypesToEventPayload<UserListEventTypes>;
};
roleTimeline: {
name: `roleTimelineStream:${MiRole['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
+ payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
};
antenna: {
name: `antennaStream:${MiAntenna['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
+ payload: EventTypesToEventPayload<AntennaEventTypes>;
};
admin: {
name: `adminStream:${MiUser['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
+ payload: EventTypesToEventPayload<AdminEventTypes>;
};
notes: {
name: 'notesStream';
@@ -287,11 +297,11 @@ export type GlobalEvents = {
};
reversi: {
name: `reversiStream:${MiUser['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
+ payload: EventTypesToEventPayload<ReversiEventTypes>;
};
reversiGame: {
name: `reversiGameStream:${MiReversiGame['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
+ payload: EventTypesToEventPayload<ReversiGameEventTypes>;
};
};
diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts
index 96d9b09992..f102461a50 100644
--- a/packages/backend/src/core/LoggerService.ts
+++ b/packages/backend/src/core/LoggerService.ts
@@ -15,7 +15,7 @@ export class LoggerService {
}
@bindThis
- public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
- return new Logger(domain, color, store);
+ public getLogger(domain: string, color?: KEYWORD | undefined) {
+ return new Logger(domain, color);
}
}
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 9786f8b8bb..74536c68f5 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -13,10 +13,12 @@ import { intersperse } from '@/misc/prelude/array.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
-import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
+import type { DefaultTreeAdapterMap } from 'parse5';
import type * as mfm from 'mfm-js';
-const treeAdapter = TreeAdapter.defaultTreeAdapter;
+const treeAdapter = parse5.defaultTreeAdapter;
+type Node = DefaultTreeAdapterMap['node'];
+type ChildNode = DefaultTreeAdapterMap['childNode'];
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
@@ -46,7 +48,7 @@ export class MfmService {
return text.trim();
- function getText(node: TreeAdapter.Node): string {
+ function getText(node: Node): string {
if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n';
@@ -58,7 +60,7 @@ export class MfmService {
return '';
}
- function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
+ function appendChildren(childNodes: ChildNode[]): void {
if (childNodes) {
for (const n of childNodes) {
analyze(n);
@@ -66,14 +68,16 @@ export class MfmService {
}
}
- function analyze(node: TreeAdapter.Node) {
+ function analyze(node: Node) {
if (treeAdapter.isTextNode(node)) {
text += node.value;
return;
}
// Skip comment or document type node
- if (!treeAdapter.isElementNode(node)) return;
+ if (!treeAdapter.isElementNode(node)) {
+ return;
+ }
switch (node.nodeName) {
case 'br': {
@@ -81,8 +85,7 @@ export class MfmService {
break;
}
- case 'a':
- {
+ case 'a': {
const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href');
@@ -90,7 +93,7 @@ export class MfmService {
// ハッシュタグ
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
text += txt;
- // メンション
+ // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
const part = txt.split('@');
@@ -102,7 +105,7 @@ export class MfmService {
} else if (part.length === 3) {
text += txt;
}
- // その他
+ // その他
} else {
const generateLink = () => {
if (!href && !txt) {
@@ -130,8 +133,7 @@ export class MfmService {
break;
}
- case 'h1':
- {
+ case 'h1': {
text += '【';
appendChildren(node.childNodes);
text += '】\n';
@@ -139,16 +141,14 @@ export class MfmService {
}
case 'b':
- case 'strong':
- {
+ case 'strong': {
text += '**';
appendChildren(node.childNodes);
text += '**';
break;
}
- case 'small':
- {
+ case 'small': {
text += '<small>';
appendChildren(node.childNodes);
text += '</small>';
@@ -156,8 +156,7 @@ export class MfmService {
}
case 's':
- case 'del':
- {
+ case 'del': {
text += '~~';
appendChildren(node.childNodes);
text += '~~';
@@ -165,8 +164,7 @@ export class MfmService {
}
case 'i':
- case 'em':
- {
+ case 'em': {
text += '<i>';
appendChildren(node.childNodes);
text += '</i>';
@@ -207,8 +205,7 @@ export class MfmService {
case 'h3':
case 'h4':
case 'h5':
- case 'h6':
- {
+ case 'h6': {
text += '\n\n';
appendChildren(node.childNodes);
break;
@@ -221,8 +218,7 @@ export class MfmService {
case 'article':
case 'li':
case 'dt':
- case 'dd':
- {
+ case 'dd': {
text += '\n';
appendChildren(node.childNodes);
break;
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index e5580f36d1..32cf3f3e26 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { AntennaService } from '@/core/AntennaService.js';
import { QueueService } from '@/core/QueueService.js';
@@ -59,7 +59,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -205,7 +204,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService,
private antennaService: AntennaService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private featuredService: FeaturedService,
private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService,
@@ -365,6 +364,9 @@ export class NoteCreateService implements OnApplicationShutdown {
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
}
+ // if the host is media-silenced, custom emojis are not allowed
+ if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
+
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
@@ -606,7 +608,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'note', {
+ this.queueService.userWebhookDeliver(webhook, 'note', {
note: noteObj,
});
}
@@ -633,7 +635,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'reply', {
+ this.queueService.userWebhookDeliver(webhook, 'reply', {
note: noteObj,
});
}
@@ -656,7 +658,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'renote', {
+ this.queueService.userWebhookDeliver(webhook, 'renote', {
note: noteObj,
});
}
@@ -788,7 +790,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'mention', {
+ this.queueService.userWebhookDeliver(webhook, 'mention', {
note: detailPackedNote,
});
}
@@ -839,7 +841,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const mentions = extractMentions(tokens);
let mentionedUsers = (await Promise.all(mentions.map(m =>
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
- ))).filter(isNotNull);
+ ))).filter(x => x != null);
// Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) =>
@@ -934,10 +936,13 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
- if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
- this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
- if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 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);
+ if (note.fileIds.length > 0) {
+ this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+ }
}
}
diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts
index 216734e9e5..b10b8e5899 100644
--- a/packages/backend/src/core/QueueModule.ts
+++ b/packages/backend/src/core/QueueModule.ts
@@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
-import { QUEUE, baseQueueOptions } from '@/queue/const.js';
+import { baseQueueOptions, QUEUE } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
+import {
+ DeliverJobData,
+ EndedPollNotificationJobData,
+ InboxJobData,
+ RelationshipJobData,
+ UserWebhookDeliverJobData,
+ SystemWebhookDeliverJobData,
+} from '../queue/types.js';
import type { Provider } from '@nestjs/common';
-import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
@@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue;
-export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
+export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
+export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
const $system: Provider = {
provide: 'queue:system',
@@ -63,9 +71,15 @@ const $objectStorage: Provider = {
inject: [DI.config],
};
-const $webhookDeliver: Provider = {
- provide: 'queue:webhookDeliver',
- useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)),
+const $userWebhookDeliver: Provider = {
+ provide: 'queue:userWebhookDeliver',
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
+ inject: [DI.config],
+};
+
+const $systemWebhookDeliver: Provider = {
+ provide: 'queue:systemWebhookDeliver',
+ useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
inject: [DI.config],
};
@@ -80,7 +94,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
- $webhookDeliver,
+ $userWebhookDeliver,
+ $systemWebhookDeliver,
],
exports: [
$system,
@@ -90,7 +105,8 @@ const $webhookDeliver: Provider = {
$db,
$relationship,
$objectStorage,
- $webhookDeliver,
+ $userWebhookDeliver,
+ $systemWebhookDeliver,
],
})
export class QueueModule implements OnApplicationShutdown {
@@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {}
public async dispose(): Promise<void> {
@@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown {
this.dbQueue.close(),
this.relationshipQueue.close(),
this.objectStorageQueue.close(),
- this.webhookDeliverQueue.close(),
+ this.userWebhookDeliverQueue.close(),
+ this.systemWebhookDeliverQueue.close(),
]);
}
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index c258a22927..80827a500b 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -8,15 +8,33 @@ import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
+import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
-import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
+import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
+import type {
+ DbJobData,
+ DeliverJobData,
+ RelationshipJobData,
+ SystemWebhookDeliverJobData,
+ ThinUser,
+ UserWebhookDeliverJobData,
+} from '../queue/types.js';
+import type {
+ DbQueue,
+ DeliverQueue,
+ EndedPollNotificationQueue,
+ InboxQueue,
+ ObjectStorageQueue,
+ RelationshipQueue,
+ SystemQueue,
+ UserWebhookDeliverQueue,
+ SystemWebhookDeliverQueue,
+} from './QueueModule.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
-import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
@Injectable()
export class QueueService {
@@ -31,7 +49,8 @@ export class QueueService {
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
this.systemQueue.add('tickCharts', {
}, {
@@ -431,9 +450,13 @@ export class QueueService {
});
}
+ /**
+ * @see UserWebhookDeliverJobData
+ * @see WebhookDeliverProcessorService
+ */
@bindThis
- public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
- const data = {
+ public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
+ const data: UserWebhookDeliverJobData = {
type,
content,
webhookId: webhook.id,
@@ -444,7 +467,33 @@ export class QueueService {
eventId: randomUUID(),
};
- return this.webhookDeliverQueue.add(webhook.id, data, {
+ return this.userWebhookDeliverQueue.add(webhook.id, data, {
+ attempts: 4,
+ backoff: {
+ type: 'custom',
+ },
+ removeOnComplete: true,
+ removeOnFail: true,
+ });
+ }
+
+ /**
+ * @see SystemWebhookDeliverJobData
+ * @see WebhookDeliverProcessorService
+ */
+ @bindThis
+ public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
+ const data: SystemWebhookDeliverJobData = {
+ type,
+ content,
+ webhookId: webhook.id,
+ to: webhook.url,
+ secret: webhook.secret,
+ createdAt: Date.now(),
+ eventId: randomUUID(),
+ };
+
+ return this.systemWebhookDeliverQueue.add(webhook.id, data, {
attempts: 4,
backoff: {
type: 'custom',
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index cb0b079df0..371207c33a 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
const FALLBACK = '\u2764';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@@ -104,6 +105,8 @@ 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);
@@ -117,11 +120,16 @@ export class ReactionService {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
}
+ // Check if note is Renote
+ if (isRenote(note) && !isQuote(note)) {
+ throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
+ }
+
let reaction = _reaction ?? FALLBACK;
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
reaction = '\u2764';
- } else if (_reaction) {
+ } else if (_reaction != null) {
const custom = reaction.match(isCustomEmojiRegexp);
if (custom) {
const reacterHost = this.utilityService.toPunyNullable(user.host);
@@ -142,6 +150,11 @@ export class ReactionService {
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
reaction = FALLBACK;
}
+
+ // for media silenced host, custom emoji reactions are not allowed
+ if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) {
+ reaction = FALLBACK;
+ }
} else {
// リアクションとして使う権限がない
reaction = FALLBACK;
@@ -214,8 +227,6 @@ export class ReactionService {
}
}
- const meta = await this.metaService.fetch();
-
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserReactionsChart.update(user, note);
}
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index d6eea70297..7966774673 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -47,6 +47,7 @@ export type RolePolicies = {
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
+ canUpdateBioMedia: boolean;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
@@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canHideAds: false,
driveCapacityMb: 100,
alwaysMarkNsfw: false,
+ canUpdateBioMedia: true,
pinLimit: 5,
antennaLimit: 5,
wordMuteLimit: 200,
@@ -376,6 +378,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
+ canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
@@ -410,14 +413,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
- public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> {
+ public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
- const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
- const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
- roleId: In(moderatorRoles.map(r => r.id)),
- }) : [];
+ const moderatorRoles = includeAdmins
+ ? roles.filter(r => r.isModerator || r.isAdministrator)
+ : roles.filter(r => r.isModerator);
+
// TODO: isRootなアカウントも含める
- return assigns.map(a => a.userId);
+ const assigns = moderatorRoles.length > 0
+ ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
+ : [];
+
+ const now = Date.now();
+ const result = [
+ // Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
+ ...new Set(
+ assigns
+ .filter(it =>
+ (excludeExpire)
+ ? (it.expiresAt == null || it.expiresAt.getTime() > now)
+ : true,
+ )
+ .map(a => a.userId),
+ ),
+ ];
+
+ return result.sort((x, y) => x.localeCompare(y));
}
@bindThis
@@ -484,14 +505,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
- if (role.isPublic) {
+ const user = await this.usersRepository.findOneByOrFail({ id: userId });
+
+ if (role.isPublic && user.host === null) {
this.notificationService.createNotification(userId, 'roleAssigned', {
roleId: roleId,
});
}
if (moderator) {
- const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', {
roleId: roleId,
roleName: role.name,
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index 5522ecd6cc..de45898328 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -21,6 +21,7 @@ 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()
export class SignupService {
@@ -35,6 +36,7 @@ export class SignupService {
private usedUsernamesRepository: UsedUsernamesRepository,
private utilityService: UtilityService,
+ private userService: UserService,
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
@@ -148,7 +150,8 @@ export class SignupService {
}));
});
- this.usersChart.update(account, true);
+ this.usersChart.update(account, true).then();
+ this.userService.notifySystemWebhook(account, 'userCreated').then();
return { account, secret };
}
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
new file mode 100644
index 0000000000..bc6851f788
--- /dev/null
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -0,0 +1,233 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { MiUser, SystemWebhooksRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
+import { IdService } from '@/core/IdService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import Logger from '@/logger.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class SystemWebhookService implements OnApplicationShutdown {
+ private logger: Logger;
+ private activeSystemWebhooksFetched = false;
+ private activeSystemWebhooks: MiSystemWebhook[] = [];
+
+ constructor(
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ @Inject(DI.systemWebhooksRepository)
+ private systemWebhooksRepository: SystemWebhooksRepository,
+ private idService: IdService,
+ private queueService: QueueService,
+ private moderationLogService: ModerationLogService,
+ private loggerService: LoggerService,
+ private globalEventService: GlobalEventService,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ this.logger = this.loggerService.getLogger('webhook');
+ }
+
+ @bindThis
+ public async fetchActiveSystemWebhooks() {
+ if (!this.activeSystemWebhooksFetched) {
+ this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({
+ isActive: true,
+ });
+ this.activeSystemWebhooksFetched = true;
+ }
+
+ return this.activeSystemWebhooks;
+ }
+
+ /**
+ * SystemWebhook の一覧を取得する.
+ */
+ @bindThis
+ public async fetchSystemWebhooks(params?: {
+ ids?: MiSystemWebhook['id'][];
+ isActive?: MiSystemWebhook['isActive'];
+ on?: MiSystemWebhook['on'];
+ }): Promise<MiSystemWebhook[]> {
+ const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook');
+ if (params) {
+ if (params.ids && params.ids.length > 0) {
+ query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids });
+ }
+ if (params.isActive !== undefined) {
+ query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive });
+ }
+ if (params.on && params.on.length > 0) {
+ query.andWhere(':on <@ systemWebhook.on', { on: params.on });
+ }
+ }
+
+ return query.getMany();
+ }
+
+ /**
+ * SystemWebhook を作成する.
+ */
+ @bindThis
+ public async createSystemWebhook(
+ params: {
+ isActive: MiSystemWebhook['isActive'];
+ name: MiSystemWebhook['name'];
+ on: MiSystemWebhook['on'];
+ url: MiSystemWebhook['url'];
+ secret: MiSystemWebhook['secret'];
+ },
+ updater: MiUser,
+ ): Promise<MiSystemWebhook> {
+ const id = this.idService.gen();
+ await this.systemWebhooksRepository.insert({
+ ...params,
+ id,
+ });
+
+ const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+ this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook);
+ this.moderationLogService
+ .log(updater, 'createSystemWebhook', {
+ systemWebhookId: webhook.id,
+ webhook: webhook,
+ })
+ .then();
+
+ return webhook;
+ }
+
+ /**
+ * SystemWebhook を更新する.
+ */
+ @bindThis
+ public async updateSystemWebhook(
+ params: {
+ id: MiSystemWebhook['id'];
+ isActive: MiSystemWebhook['isActive'];
+ name: MiSystemWebhook['name'];
+ on: MiSystemWebhook['on'];
+ url: MiSystemWebhook['url'];
+ secret: MiSystemWebhook['secret'];
+ },
+ updater: MiUser,
+ ): Promise<MiSystemWebhook> {
+ const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id });
+ await this.systemWebhooksRepository.update(beforeEntity.id, {
+ updatedAt: new Date(),
+ isActive: params.isActive,
+ name: params.name,
+ on: params.on,
+ url: params.url,
+ secret: params.secret,
+ });
+
+ const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id });
+ this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity);
+ this.moderationLogService
+ .log(updater, 'updateSystemWebhook', {
+ systemWebhookId: beforeEntity.id,
+ before: beforeEntity,
+ after: afterEntity,
+ })
+ .then();
+
+ return afterEntity;
+ }
+
+ /**
+ * SystemWebhook を削除する.
+ */
+ @bindThis
+ public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) {
+ const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id });
+ await this.systemWebhooksRepository.delete(id);
+
+ this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook);
+ this.moderationLogService
+ .log(updater, 'deleteSystemWebhook', {
+ systemWebhookId: webhook.id,
+ webhook,
+ })
+ .then();
+ }
+
+ /**
+ * SystemWebhook をWebhook配送キューに追加する
+ * @see QueueService.systemWebhookDeliver
+ */
+ @bindThis
+ public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
+ const webhookEntity = typeof webhook === 'string'
+ ? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
+ : webhook;
+ if (!webhookEntity || !webhookEntity.isActive) {
+ this.logger.info(`Webhook is not active or not found : ${webhook}`);
+ return;
+ }
+
+ if (!webhookEntity.on.includes(type)) {
+ this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
+ return;
+ }
+
+ return this.queueService.systemWebhookDeliver(webhookEntity, type, content);
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise<void> {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'systemWebhookCreated': {
+ if (body.isActive) {
+ this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+ }
+ break;
+ }
+ case 'systemWebhookUpdated': {
+ if (body.isActive) {
+ const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id);
+ if (i > -1) {
+ this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body);
+ } else {
+ this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body));
+ }
+ } else {
+ this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+ }
+ break;
+ }
+ case 'systemWebhookDeleted': {
+ this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts
index 96f389b54c..2f1310b8ef 100644
--- a/packages/backend/src/core/UserBlockingService.ts
+++ b/packages/backend/src/core/UserBlockingService.ts
@@ -16,7 +16,7 @@ import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit {
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private loggerService: LoggerService,
) {
@@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 406ea04031..6aab8fde70 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -16,7 +16,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import type { Packed } from '@/misc/json-schema.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
-import { WebhookService } from '@/core/WebhookService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit {
private metaService: MetaService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
- private webhookService: WebhookService,
+ private webhookService: UserWebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
private fanoutTimelineService: FanoutTimelineService,
@@ -279,8 +279,10 @@ export class UserFollowingService implements OnModuleInit {
});
// 通知を作成
- this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
- }, followee.id);
+ if (follower.host === null) {
+ this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
+ }, followee.id);
+ }
}
if (alreadyFollowed) return;
@@ -331,7 +333,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'follow', {
+ this.queueService.userWebhookDeliver(webhook, 'follow', {
user: packed,
});
}
@@ -345,7 +347,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'followed', {
+ this.queueService.userWebhookDeliver(webhook, 'followed', {
user: packed,
});
}
@@ -398,7 +400,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
@@ -740,7 +742,7 @@ export class UserFollowingService implements OnModuleInit {
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
- this.queueService.webhookDeliver(webhook, 'unfollow', {
+ this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packedFollowee,
});
}
diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts
index bbdcfed738..6333356fe9 100644
--- a/packages/backend/src/core/UserListService.ts
+++ b/packages/backend/src/core/UserListService.ts
@@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
const currentCount = await this.userListMembershipsRepository.countBy({
userListId: list.id,
});
- if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
+ if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
throw new UserListService.TooManyUsersError();
}
diff --git a/packages/backend/src/core/UserRenoteMutingService.ts b/packages/backend/src/core/UserRenoteMutingService.ts
new file mode 100644
index 0000000000..bdc5e23f4b
--- /dev/null
+++ b/packages/backend/src/core/UserRenoteMutingService.ts
@@ -0,0 +1,52 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import type { RenoteMutingsRepository } from '@/models/_.js';
+import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
+
+import { IdService } from '@/core/IdService.js';
+import type { MiUser } from '@/models/User.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { CacheService } from '@/core/CacheService.js';
+
+@Injectable()
+export class UserRenoteMutingService {
+ constructor(
+ @Inject(DI.renoteMutingsRepository)
+ private renoteMutingsRepository: RenoteMutingsRepository,
+
+ private idService: IdService,
+ private cacheService: CacheService,
+ ) {
+ }
+
+ @bindThis
+ public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
+ await this.renoteMutingsRepository.insert({
+ id: this.idService.gen(),
+ muterId: user.id,
+ muteeId: target.id,
+ });
+
+ await this.cacheService.renoteMutingsCache.refresh(user.id);
+ }
+
+ @bindThis
+ public async unmute(mutings: MiRenoteMuting[]): Promise<void> {
+ if (mutings.length === 0) return;
+
+ await this.renoteMutingsRepository.delete({
+ id: In(mutings.map(m => m.id)),
+ });
+
+ const muterIds = [...new Set(mutings.map(m => m.muterId))];
+ for (const muterId of muterIds) {
+ await this.cacheService.renoteMutingsCache.refresh(muterId);
+ }
+ }
+}
diff --git a/packages/backend/src/core/UserSearchService.ts b/packages/backend/src/core/UserSearchService.ts
new file mode 100644
index 0000000000..0d03cf6ee0
--- /dev/null
+++ b/packages/backend/src/core/UserSearchService.ts
@@ -0,0 +1,205 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import type { Config } from '@/config.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { Packed } from '@/misc/json-schema.js';
+
+function defaultActiveThreshold() {
+ return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
+}
+
+@Injectable()
+export class UserSearchService {
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+ @Inject(DI.followingsRepository)
+ private followingsRepository: FollowingsRepository,
+ private userEntityService: UserEntityService,
+ ) {
+ }
+
+ /**
+ * ユーザ名とホスト名によるユーザ検索を行う.
+ *
+ * - 検索結果には優先順位がつけられており、以下の順序で検索が行われる.
+ * 1. フォローしているユーザのうち、一定期間以内(※)に更新されたユーザ
+ * 2. フォローしているユーザのうち、一定期間以内に更新されていないユーザ
+ * 3. フォローしていないユーザのうち、一定期間以内に更新されたユーザ
+ * 4. フォローしていないユーザのうち、一定期間以内に更新されていないユーザ
+ * - ログインしていない場合は、以下の順序で検索が行われる.
+ * 1. 一定期間以内に更新されたユーザ
+ * 2. 一定期間以内に更新されていないユーザ
+ * - それぞれの検索結果はユーザ名の昇順でソートされる.
+ * - 動作的には先に登場した検索結果の登場位置が優先される(条件的にユーザIDが重複することはないが).
+ * (1で既にヒットしていた場合、2, 3, 4でヒットしても無視される)
+ * - ユーザ名とホスト名の検索条件はそれぞれ前方一致で検索される.
+ * - ユーザ名の検索は大文字小文字を区別しない.
+ * - ホスト名の検索は大文字小文字を区別しない.
+ * - 検索結果は最大で {@link opts.limit} 件までとなる.
+ *
+ * ※一定期間とは {@link params.activeThreshold} で指定された日時から現在までの期間を指す.
+ *
+ * @param params 検索条件.
+ * @param opts 関数の動作を制御するオプション.
+ * @param me 検索を実行するユーザの情報. 未ログインの場合は指定しない.
+ * @see {@link UserSearchService#buildSearchUserQueries}
+ * @see {@link UserSearchService#buildSearchUserNoLoginQueries}
+ */
+ @bindThis
+ public async search(
+ params: {
+ username?: string | null,
+ host?: string | null,
+ activeThreshold?: Date,
+ },
+ opts?: {
+ limit?: number,
+ detail?: boolean,
+ },
+ me?: MiUser | null,
+ ): Promise<Packed<'User'>[]> {
+ const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params);
+
+ let resultSet = new Set<MiUser['id']>();
+ const limit = opts?.limit ?? 10;
+ for (const query of queries) {
+ const ids = await query
+ .select('user.id')
+ .limit(limit - resultSet.size)
+ .orderBy('user.usernameLower', 'ASC')
+ .getRawMany<{ user_id: MiUser['id'] }>()
+ .then(res => res.map(x => x.user_id));
+
+ resultSet = new Set([...resultSet, ...ids]);
+ if (resultSet.size >= limit) {
+ break;
+ }
+ }
+
+ return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>(
+ [...resultSet].slice(0, limit),
+ me,
+ { schema: opts?.detail ? 'UserDetailed' : 'UserLite' },
+ );
+ }
+
+ /**
+ * ログイン済みユーザによる検索実行時のクエリ一覧を構築する.
+ * @param me
+ * @param params
+ * @private
+ */
+ @bindThis
+ private buildSearchUserQueries(
+ me: MiUser,
+ params: {
+ username?: string | null,
+ host?: string | null,
+ activeThreshold?: Date,
+ },
+ ) {
+ // デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
+ const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
+
+ const followingUserQuery = this.followingsRepository.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
+
+ const activeFollowingUsersQuery = this.generateUserQueryBuilder(params)
+ .andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
+ .andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
+ activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
+
+ const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params)
+ .andWhere(`user.id IN (${followingUserQuery.getQuery()})`)
+ .andWhere(new Brackets(qb => {
+ qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
+ }));
+ inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters());
+
+ // 自分自身がヒットするとしたらここ
+ const activeUserQuery = this.generateUserQueryBuilder(params)
+ .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
+ .andWhere('user.updatedAt > :activeThreshold', { activeThreshold });
+ activeUserQuery.setParameters(followingUserQuery.getParameters());
+
+ const inactiveUserQuery = this.generateUserQueryBuilder(params)
+ .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`)
+ .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
+ inactiveUserQuery.setParameters(followingUserQuery.getParameters());
+
+ return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery];
+ }
+
+ /**
+ * ログインしていないユーザによる検索実行時のクエリ一覧を構築する.
+ * @param params
+ * @private
+ */
+ @bindThis
+ private buildSearchUserNoLoginQueries(params: {
+ username?: string | null,
+ host?: string | null,
+ activeThreshold?: Date,
+ }) {
+ // デフォルト30日以内に更新されたユーザーをアクティブユーザーとする
+ const activeThreshold = params.activeThreshold ?? defaultActiveThreshold();
+
+ const activeUserQuery = this.generateUserQueryBuilder(params)
+ .andWhere(new Brackets(qb => {
+ qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
+ }));
+
+ const inactiveUserQuery = this.generateUserQueryBuilder(params)
+ .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold });
+
+ return [activeUserQuery, inactiveUserQuery];
+ }
+
+ /**
+ * ユーザ検索クエリで共通する抽出条件をあらかじめ設定したクエリビルダを生成する.
+ * @param params
+ * @private
+ */
+ @bindThis
+ private generateUserQueryBuilder(params: {
+ username?: string | null,
+ host?: string | null,
+ }): SelectQueryBuilder<MiUser> {
+ const userQuery = this.usersRepository.createQueryBuilder('user');
+
+ if (params.username) {
+ userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' });
+ }
+
+ if (params.host) {
+ if (params.host === this.config.hostname || params.host === '.') {
+ userQuery.andWhere('user.host IS NULL');
+ } else {
+ userQuery.andWhere('user.host LIKE :host', {
+ host: sqlLikeEscape(params.host.toLowerCase()) + '%',
+ });
+ }
+ }
+
+ userQuery.andWhere('user.isSuspended = FALSE');
+
+ return userQuery;
+ }
+}
diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts
index 72fa4d928d..9b1961c631 100644
--- a/packages/backend/src/core/UserService.ts
+++ b/packages/backend/src/core/UserService.ts
@@ -8,15 +8,18 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
@Injectable()
export class UserService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
-
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
+ private systemWebhookService: SystemWebhookService,
+ private userEntityService: UserEntityService,
) {
}
@@ -50,4 +53,23 @@ export class UserService {
});
}
}
+
+ /**
+ * SystemWebhookを用いてユーザに関する操作内容を管理者各位に通知する.
+ * ここではJobQueueへのエンキューのみを行うため、即時実行されない.
+ *
+ * @see SystemWebhookService.enqueueSystemWebhook
+ */
+ @bindThis
+ public async notifySystemWebhook(user: MiUser, type: 'userCreated') {
+ const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' });
+ const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] });
+ for (const webhookId of recipientWebhookIds) {
+ await this.systemWebhookService.enqueueSystemWebhook(
+ webhookId,
+ type,
+ packedUser,
+ );
+ }
+ }
}
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
new file mode 100644
index 0000000000..e96bfeea95
--- /dev/null
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { WebhooksRepository } from '@/models/_.js';
+import type { MiWebhook } from '@/models/Webhook.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+@Injectable()
+export class UserWebhookService implements OnApplicationShutdown {
+ private activeWebhooksFetched = false;
+ private activeWebhooks: MiWebhook[] = [];
+
+ constructor(
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+ @Inject(DI.webhooksRepository)
+ private webhooksRepository: WebhooksRepository,
+ ) {
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ @bindThis
+ public async getActiveWebhooks() {
+ if (!this.activeWebhooksFetched) {
+ this.activeWebhooks = await this.webhooksRepository.findBy({
+ active: true,
+ });
+ this.activeWebhooksFetched = true;
+ }
+
+ return this.activeWebhooks;
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise<void> {
+ const obj = JSON.parse(data);
+ if (obj.channel !== 'internal') {
+ return;
+ }
+
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'webhookCreated': {
+ if (body.active) {
+ this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ });
+ }
+ break;
+ }
+ case 'webhookUpdated': {
+ if (body.active) {
+ const i = this.activeWebhooks.findIndex(a => a.id === body.id);
+ if (i > -1) {
+ this.activeWebhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ };
+ } else {
+ this.activeWebhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
+ ...body,
+ latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
+ user: null, // joinなカラムは通常取ってこないので
+ });
+ }
+ } else {
+ this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+ }
+ break;
+ }
+ case 'webhookDeleted': {
+ this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
+}
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 652e8f7449..94729250a6 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -43,6 +43,12 @@ export class UtilityService {
}
@bindThis
+ public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
+ if (!silencedHosts || host == null) return false;
+ return silencedHosts.some(x => host.toLowerCase() === x);
+ }
+
+ @bindThis
public concatNoteContentsForKeyWordCheck(content: {
cw?: string | null;
text?: string | null;
diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts
deleted file mode 100644
index 6be34977b0..0000000000
--- a/packages/backend/src/core/WebhookService.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
-import type { WebhooksRepository } from '@/models/_.js';
-import type { MiWebhook } from '@/models/Webhook.js';
-import { DI } from '@/di-symbols.js';
-import { bindThis } from '@/decorators.js';
-import type { GlobalEvents } from '@/core/GlobalEventService.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
-
-@Injectable()
-export class WebhookService implements OnApplicationShutdown {
- private webhooksFetched = false;
- private webhooks: MiWebhook[] = [];
-
- constructor(
- @Inject(DI.redisForSub)
- private redisForSub: Redis.Redis,
-
- @Inject(DI.webhooksRepository)
- private webhooksRepository: WebhooksRepository,
- ) {
- //this.onMessage = this.onMessage.bind(this);
- this.redisForSub.on('message', this.onMessage);
- }
-
- @bindThis
- public async getActiveWebhooks() {
- if (!this.webhooksFetched) {
- this.webhooks = await this.webhooksRepository.findBy({
- active: true,
- });
- this.webhooksFetched = true;
- }
-
- return this.webhooks;
- }
-
- @bindThis
- private async onMessage(_: string, data: string): Promise<void> {
- const obj = JSON.parse(data);
-
- if (obj.channel === 'internal') {
- const { type, body } = obj.message as GlobalEvents['internal']['payload'];
- switch (type) {
- case 'webhookCreated':
- if (body.active) {
- this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- });
- }
- break;
- case 'webhookUpdated':
- if (body.active) {
- const i = this.webhooks.findIndex(a => a.id === body.id);
- if (i > -1) {
- this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- };
- } else {
- this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
- ...body,
- latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
- user: null, // joinなカラムは通常取ってこないので
- });
- }
- } else {
- this.webhooks = this.webhooks.filter(a => a.id !== body.id);
- }
- break;
- case 'webhookDeleted':
- this.webhooks = this.webhooks.filter(a => a.id !== body.id);
- break;
- default:
- break;
- }
- }
- }
-
- @bindThis
- public dispose(): void {
- this.redisForSub.off('message', this.onMessage);
- }
-
- @bindThis
- public onApplicationShutdown(signal?: string | undefined): void {
- this.dispose();
- }
-}
diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts
index 0fccc7b950..5a5a76f7d6 100644
--- a/packages/backend/src/core/activitypub/ApAudienceService.ts
+++ b/packages/backend/src/core/activitypub/ApAudienceService.ts
@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { concat, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { getApIds } from './type.js';
import { ApPersonService } from './models/ApPersonService.js';
import type { ApObject } from './type.js';
@@ -41,7 +40,7 @@ export class ApAudienceService {
const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
- )).filter(isNotNull);
+ )).filter(x => x != null);
if (toGroups.public.length > 0) {
return {
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index d0d206760c..e2164fec1d 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -27,8 +27,8 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
@@ -57,9 +57,6 @@ export class ApInboxService {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
- @Inject(DI.abuseUserReportsRepository)
- private abuseUserReportsRepository: AbuseUserReportsRepository,
-
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@@ -68,6 +65,7 @@ export class ApInboxService {
private utilityService: UtilityService,
private idService: IdService,
private metaService: MetaService,
+ private abuseReportService: AbuseReportService,
private userFollowingService: UserFollowingService,
private apAudienceService: ApAudienceService,
private reactionService: ReactionService,
@@ -539,20 +537,19 @@ export class ApInboxService {
const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1))
- .filter(isNotNull);
+ .filter(x => x != null);
const users = await this.usersRepository.findBy({
id: In(userIds),
});
if (users.length < 1) return 'skip';
- await this.abuseUserReportsRepository.insert({
- id: this.idService.gen(),
+ await this.abuseReportService.report([{
targetUserId: users[0].id,
targetUserHost: users[0].host,
reporterId: actor.id,
reporterHost: actor.host,
comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`,
- });
+ }]);
return 'ok';
}
diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts
index ab75b9abbd..4036d2794a 100644
--- a/packages/backend/src/core/activitypub/ApMfmService.ts
+++ b/packages/backend/src/core/activitypub/ApMfmService.ts
@@ -25,7 +25,7 @@ export class ApMfmService {
}
@bindThis
- public getNoteHtml(note: MiNote, apAppend?: string) {
+ public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
let noMisskeyContent = false;
const srcMfm = (note.text ?? '') + (apAppend ?? '');
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 4fc724b548..98e944f347 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -26,7 +26,6 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js';
import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
@@ -317,7 +316,7 @@ export class ApRendererService {
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
- return ids.map(id => items.find(item => item.id === id)).filter(isNotNull);
+ return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
};
let inReplyTo;
@@ -686,7 +685,7 @@ export class ApRendererService {
if (names.length === 0) return [];
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
- const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
+ const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null);
return emojis;
}
diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts
index 0ced7e88af..2cd151fa04 100644
--- a/packages/backend/src/core/activitypub/models/ApMentionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts
@@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiUser } from '@/models/_.js';
import { toArray, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { isMention } from '../type.js';
import { Resolver } from '../ApResolverService.js';
import { ApPersonService } from './ApPersonService.js';
@@ -28,7 +27,7 @@ export class ApMentionService {
const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
- )).filter(isNotNull);
+ )).filter(x => x != null);
return mentionedUsers;
}
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index c6e6b3a1e8..fc7aa1e0b9 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -24,7 +24,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
@@ -253,7 +252,7 @@ export class ApNoteService {
}
};
- const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
+ const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
const results = await Promise.all(uris.map(tryResolveNote));
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 744b1ea683..457205e023 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -34,11 +34,11 @@ import { StatusError } from '@/misc/status-error.js';
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';
-import { isNotNull } from '@/misc/is-not-null.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -101,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
+
+ private roleService: RoleService,
) {
}
@@ -239,6 +241,11 @@ export class ApPersonService implements OnModuleInit {
return this.apImageService.resolveImage(user, img).catch(() => null);
}));
+ if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null))
+ && !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) {
+ return {};
+ }
+
/*
we don't want to return nulls on errors! if the database fields
are already null, nothing changes; if the database has old
@@ -637,7 +644,7 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持
let td = 0;
- for (const note of featuredNotes.filter(isNotNull)) {
+ for (const note of featuredNotes.filter(x => x != null)) {
td -= 1000;
transactionalEntityManager.insert(MiUserNotePining, {
id: this.idService.gen(Date.now() + td),
diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
index d1936cfe1d..73004d10b0 100644
--- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
@@ -10,7 +10,6 @@ import type { Config } from '@/config.js';
import type { IPoll } from '@/models/Poll.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js';
@@ -52,7 +51,7 @@ export class ApQuestionService {
const choices = question[multiple ? 'anyOf' : 'oneOf']
?.map((x) => x.name)
- .filter(isNotNull)
+ .filter(x => x != null)
?? [];
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);
@@ -75,10 +74,10 @@ export class ApQuestionService {
//#region このサーバーに既に登録されているか
const note = await this.notesRepository.findOneBy({ uri });
- if (note == null) throw new Error('Question is not registed');
+ if (note == null) throw new Error('Question is not registered');
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
- if (poll == null) throw new Error('Question is not registed');
+ if (poll == null) throw new Error('Question is not registered');
//#endregion
// resolve new Question object
diff --git a/packages/backend/src/core/activitypub/models/tag.ts b/packages/backend/src/core/activitypub/models/tag.ts
index e7ceec3262..f75cc45f7e 100644
--- a/packages/backend/src/core/activitypub/models/tag.ts
+++ b/packages/backend/src/core/activitypub/models/tag.ts
@@ -4,7 +4,6 @@
*/
import { toArray } from '@/misc/prelude/array.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js';
@@ -16,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
return hashtags.map(tag => {
const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null;
- }).filter(isNotNull);
+ }).filter(x => x != null);
}
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts
index afc728d564..20815ea968 100644
--- a/packages/backend/src/core/chart/ChartLoggerService.ts
+++ b/packages/backend/src/core/chart/ChartLoggerService.ts
@@ -14,6 +14,6 @@ export class ChartLoggerService {
constructor(
private loggerService: LoggerService,
) {
- this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test');
+ this.logger = this.loggerService.getLogger('chart', 'white');
}
}
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index 5e4555ee96..c2329a2f73 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -47,7 +47,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
.select('instance.host')
- .where('instance.isSuspended = true');
+ .where('instance.suspensionState != \'none\'');
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
.select('f.followerHost')
@@ -89,7 +89,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
- .andWhere('instance.isSuspended = false')
+ .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),
@@ -97,7 +97,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
.select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
- .andWhere('instance.isSuspended = false')
+ .andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
.then(x => parseInt(x.count, 10)),
diff --git a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
new file mode 100644
index 0000000000..1e23c194c5
--- /dev/null
+++ b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts
@@ -0,0 +1,87 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { Packed } from '@/misc/json-schema.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+
+@Injectable()
+export class AbuseReportNotificationRecipientEntityService {
+ constructor(
+ @Inject(DI.abuseReportNotificationRecipientRepository)
+ private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository,
+ private userEntityService: UserEntityService,
+ private systemWebhookEntityService: SystemWebhookEntityService,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient,
+ opts?: {
+ users: Map<string, Packed<'UserLite'>>,
+ webhooks: Map<string, Packed<'SystemWebhook'>>,
+ },
+ ): Promise<Packed<'AbuseReportNotificationRecipient'>> {
+ const recipient = typeof src === 'object'
+ ? src
+ : await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: src });
+ const user = recipient.userId
+ ? (opts?.users.get(recipient.userId) ?? await this.userEntityService.pack<'UserLite'>(recipient.userId))
+ : undefined;
+ const webhook = recipient.systemWebhookId
+ ? (opts?.webhooks.get(recipient.systemWebhookId) ?? await this.systemWebhookEntityService.pack(recipient.systemWebhookId))
+ : undefined;
+
+ return {
+ id: recipient.id,
+ isActive: recipient.isActive,
+ updatedAt: recipient.updatedAt.toISOString(),
+ name: recipient.name,
+ method: recipient.method,
+ userId: recipient.userId ?? undefined,
+ user: user,
+ systemWebhookId: recipient.systemWebhookId ?? undefined,
+ systemWebhook: webhook,
+ };
+ }
+
+ @bindThis
+ public async packMany(
+ src: MiAbuseReportNotificationRecipient['id'][] | MiAbuseReportNotificationRecipient[],
+ ): Promise<Packed<'AbuseReportNotificationRecipient'>[]> {
+ const objs = src.filter((it): it is MiAbuseReportNotificationRecipient => typeof it === 'object');
+ const ids = src.filter((it): it is MiAbuseReportNotificationRecipient['id'] => typeof it === 'string');
+ if (ids.length > 0) {
+ objs.push(
+ ...await this.abuseReportNotificationRecipientRepository.findBy({ id: In(ids) }),
+ );
+ }
+
+ const userIds = objs.map(it => it.userId).filter(x => x != null);
+ const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0)
+ ? await this.userEntityService.packMany(userIds)
+ .then(it => new Map(it.map(it => [it.id, it])))
+ : new Map();
+
+ const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null);
+ const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (systemWebhookIds.length > 0)
+ ? await this.systemWebhookEntityService.packMany(systemWebhookIds)
+ .then(it => new Map(it.map(it => [it.id, it])))
+ : new Map();
+
+ return Promise
+ .all(
+ objs.map(it => this.pack(it, { users: users, webhooks: systemWebhooks })),
+ )
+ .then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+ }
+}
+
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index b0e1d1ab36..a13c244c19 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -10,7 +10,6 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@@ -63,7 +62,7 @@ export class AbuseUserReportEntityService {
) {
const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
- const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull);
+ const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null);
const _userMap = await this.userEntityService.packMany(
[..._reporters, ..._targetUsers, ..._assignees],
null,
diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts
index 3855a28436..d915645906 100644
--- a/packages/backend/src/core/entities/ClipEntityService.ts
+++ b/packages/backend/src/core/entities/ClipEntityService.ts
@@ -53,7 +53,7 @@ export class ClipEntityService {
isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
- notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
+ notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
});
}
diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts
index 02ff2e7754..c485555f90 100644
--- a/packages/backend/src/core/entities/DriveFileEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFileEntityService.ts
@@ -16,7 +16,6 @@ import { appendQuery, query } from '@/misc/prelude/url.js';
import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js';
import { UtilityService } from '../UtilityService.js';
import { VideoProcessingService } from '../VideoProcessingService.js';
@@ -261,11 +260,11 @@ export class DriveFileEntityService {
files: MiDriveFile[],
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
- const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull);
+ const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
const _userMap = await this.userEntityService.packMany(_user)
.then(users => new Map(users.map(user => [user.id, user])));
const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
- return items.filter(isNotNull);
+ return items.filter(x => x != null);
}
@bindThis
@@ -290,6 +289,6 @@ export class DriveFileEntityService {
): Promise<Packed<'DriveFile'>[]> {
if (fileIds.length === 0) return [];
const filesMap = await this.packManyByIdsMap(fileIds, options);
- return fileIds.map(id => filesMap.get(id)).filter(isNotNull);
+ return fileIds.map(id => filesMap.get(id)).filter(x => x != null);
}
}
diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts
index 9117b13914..4c45c13167 100644
--- a/packages/backend/src/core/entities/InstanceEntityService.ts
+++ b/packages/backend/src/core/entities/InstanceEntityService.ts
@@ -50,6 +50,7 @@ export class InstanceEntityService {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
+ isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host),
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
diff --git a/packages/backend/src/core/entities/InviteCodeEntityService.ts b/packages/backend/src/core/entities/InviteCodeEntityService.ts
index 26f57e1299..5d3e823a2a 100644
--- a/packages/backend/src/core/entities/InviteCodeEntityService.ts
+++ b/packages/backend/src/core/entities/InviteCodeEntityService.ts
@@ -12,7 +12,6 @@ import type { MiUser } from '@/models/User.js';
import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -59,8 +58,8 @@ export class InviteCodeEntityService {
tickets: MiRegistrationTicket[],
me: { id: MiUser['id'] },
) {
- const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull);
- const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull);
+ const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null);
+ const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null);
const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 5dfec589e1..44ec0d6a7b 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -50,6 +50,22 @@ export class MetaEntityService {
}))
.getMany();
+ // クライアントの手間を減らすためあらかじめJSONに変換しておく
+ let defaultLightTheme = null;
+ let defaultDarkTheme = null;
+ if (instance.defaultLightTheme) {
+ try {
+ defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
+ } catch (e) {
+ }
+ }
+ if (instance.defaultDarkTheme) {
+ try {
+ defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
+ } catch (e) {
+ }
+ }
+
const packed: Packed<'MetaLite'> = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
@@ -90,9 +106,8 @@ export class MetaEntityService {
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
- // クライアントの手間を減らすためあらかじめJSONに変換しておく
- defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
- defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
+ defaultLightTheme,
+ defaultDarkTheme,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
@@ -113,6 +128,7 @@ export class MetaEntityService {
mediaProxy: this.config.mediaProxy,
enableUrlPreview: instance.urlPreviewEnabled,
+ noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
};
return packed;
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 2ce72c50b8..2cd092231c 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -14,7 +14,6 @@ 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 { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -276,7 +275,7 @@ export class NoteEntityService implements OnModuleInit {
packedFiles.set(k, v);
}
}
- return fileIds.map(id => packedFiles.get(id)).filter(isNotNull);
+ return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
}
@bindThis
@@ -449,12 +448,12 @@ export class NoteEntityService implements OnModuleInit {
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
- const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
+ const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
const users = [
...notes.map(({ user, userId }) => user ?? userId),
- ...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull),
- ...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull),
+ ...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null),
+ ...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
];
const packedUsers = await this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u])));
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index 94d56c883b..f393513510 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -13,7 +13,6 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio
import type { MiNote } from '@/models/Note.js';
import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
import { CacheService } from '@/core/CacheService.js';
import { RoleEntityService } from './RoleEntityService.js';
@@ -103,7 +102,7 @@ export class NotificationEntityService implements OnModuleInit {
user,
reaction: reaction.reaction,
};
- }))).filter(r => isNotNull(r.user));
+ }))).filter(r => r.user != null);
// if all users have been deleted, don't show this notification
if (reactions.length === 0) {
return null;
@@ -124,7 +123,7 @@ export class NotificationEntityService implements OnModuleInit {
}
return this.userEntityService.pack(userId, { id: meId });
- }))).filter(isNotNull);
+ }))).filter(x => x != null);
// if all users have been deleted, don't show this notification
if (users.length === 0) {
return null;
@@ -181,7 +180,7 @@ export class NotificationEntityService implements OnModuleInit {
validNotifications = await this.#filterValidNotifier(validNotifications, meId);
- const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
+ const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null);
const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) },
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
@@ -223,7 +222,7 @@ export class NotificationEntityService implements OnModuleInit {
);
});
- return (await Promise.all(packPromises)).filter(isNotNull);
+ return (await Promise.all(packPromises)).filter(x => x != null);
}
@bindThis
@@ -305,7 +304,7 @@ export class NotificationEntityService implements OnModuleInit {
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
]);
- const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull);
+ const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null);
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
where: { id: In(notifierIds) },
}) : [];
@@ -313,7 +312,7 @@ export class NotificationEntityService implements OnModuleInit {
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
return isValid ? notification : null;
- }))) as [T | null] ).filter(isNotNull);
+ }))) as [T | null] ).filter(x => x != null);
return filteredNotifications;
}
diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts
index 142d9e81db..46bf51bb6d 100644
--- a/packages/backend/src/core/entities/PageEntityService.ts
+++ b/packages/backend/src/core/entities/PageEntityService.ts
@@ -14,7 +14,6 @@ import type { MiPage } from '@/models/Page.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
@@ -106,7 +105,7 @@ export class PageEntityService {
script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
- attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)),
+ attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)),
likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
});
diff --git a/packages/backend/src/core/entities/SystemWebhookEntityService.ts b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
new file mode 100644
index 0000000000..e18734091c
--- /dev/null
+++ b/packages/backend/src/core/entities/SystemWebhookEntityService.ts
@@ -0,0 +1,74 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { MiSystemWebhook, SystemWebhooksRepository } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { Packed } from '@/misc/json-schema.js';
+
+@Injectable()
+export class SystemWebhookEntityService {
+ constructor(
+ @Inject(DI.systemWebhooksRepository)
+ private systemWebhooksRepository: SystemWebhooksRepository,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: MiSystemWebhook['id'] | MiSystemWebhook,
+ opts?: {
+ webhooks: Map<string, MiSystemWebhook>
+ },
+ ): Promise<Packed<'SystemWebhook'>> {
+ const webhook = typeof src === 'object'
+ ? src
+ : opts?.webhooks.get(src) ?? await this.systemWebhooksRepository.findOneByOrFail({ id: src });
+
+ return {
+ id: webhook.id,
+ isActive: webhook.isActive,
+ updatedAt: webhook.updatedAt.toISOString(),
+ latestSentAt: webhook.latestSentAt?.toISOString() ?? null,
+ latestStatus: webhook.latestStatus,
+ name: webhook.name,
+ on: webhook.on,
+ url: webhook.url,
+ secret: webhook.secret,
+ };
+ }
+
+ @bindThis
+ public async packMany(src: MiSystemWebhook['id'][] | MiSystemWebhook[]): Promise<Packed<'SystemWebhook'>[]> {
+ if (src.length === 0) {
+ return [];
+ }
+
+ const webhooks = Array.of<MiSystemWebhook>();
+ webhooks.push(
+ ...src.filter((it): it is MiSystemWebhook => typeof it === 'object'),
+ );
+
+ const ids = src.filter((it): it is MiSystemWebhook['id'] => typeof it === 'string');
+ if (ids.length > 0) {
+ webhooks.push(
+ ...await this.systemWebhooksRepository.findBy({ id: In(ids) }),
+ );
+ }
+
+ return Promise
+ .all(
+ webhooks.map(x =>
+ this.pack(x, {
+ webhooks: new Map(webhooks.map(x => [x.id, x])),
+ }),
+ ),
+ )
+ .then(it => it.sort((a, b) => a.id.localeCompare(b.id)));
+ }
+}
+
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index b80a1ec206..7fd093c191 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -47,7 +47,6 @@ import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
-import { isNotNull } from '@/misc/is-not-null.js';
import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
@@ -502,11 +501,15 @@ export class UserEntityService implements OnModuleInit {
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
// パフォーマンス上の理由でローカルユーザーのみ
- badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({
- name: r.name,
- iconUrl: r.iconUrl,
- displayOrder: r.displayOrder,
- }))) : undefined,
+ badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
+ .filter((r) => r.isPublic || iAmModerator)
+ .sort((a, b) => b.displayOrder - a.displayOrder)
+ .map((r) => ({
+ name: r.name,
+ iconUrl: r.iconUrl,
+ displayOrder: r.displayOrder,
+ }))
+ ) : undefined,
...(isDetailed ? {
url: profile!.url,
@@ -514,7 +517,7 @@ export class UserEntityService implements OnModuleInit {
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
alsoKnownAs: user.alsoKnownAs
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
- .then(xs => xs.length === 0 ? null : xs.filter(isNotNull))
+ .then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
: null,
createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 919f4794a3..271082b4ff 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -49,6 +49,7 @@ export const DI = {
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
hashtagsRepository: Symbol('hashtagsRepository'),
abuseUserReportsRepository: Symbol('abuseUserReportsRepository'),
+ abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'),
registrationTicketsRepository: Symbol('registrationTicketsRepository'),
authSessionsRepository: Symbol('authSessionsRepository'),
accessTokensRepository: Symbol('accessTokensRepository'),
@@ -70,6 +71,7 @@ export const DI = {
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
registryItemsRepository: Symbol('registryItemsRepository'),
webhooksRepository: Symbol('webhooksRepository'),
+ systemWebhooksRepository: Symbol('systemWebhooksRepository'),
adsRepository: Symbol('adsRepository'),
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts
index d4705af601..ff5363a425 100644
--- a/packages/backend/src/logger.ts
+++ b/packages/backend/src/logger.ts
@@ -22,31 +22,27 @@ type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
export default class Logger {
private context: Context;
private parentLogger: Logger | null = null;
- private store: boolean;
- constructor(context: string, color?: KEYWORD, store = true) {
+ constructor(context: string, color?: KEYWORD) {
this.context = {
name: context,
color: color,
};
- this.store = store;
}
@bindThis
- public createSubLogger(context: string, color?: KEYWORD, store = true): Logger {
- const logger = new Logger(context, color, store);
+ public createSubLogger(context: string, color?: KEYWORD): Logger {
+ const logger = new Logger(context, color);
logger.parentLogger = this;
return logger;
}
@bindThis
- private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = [], store = true): void {
+ private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = []): void {
if (envOption.quiet) return;
- if (!this.store) store = false;
- if (level === 'debug') store = false;
if (this.parentLogger) {
- this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts), store);
+ this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts));
return;
}
diff --git a/packages/backend/src/misc/is-not-null.ts b/packages/backend/src/misc/is-not-null.ts
deleted file mode 100644
index 8d9dc8bb39..0000000000
--- a/packages/backend/src/misc/is-not-null.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T {
- return input != null;
-}
diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts
index 93c9b2b814..862d6e6a38 100644
--- a/packages/backend/src/misc/is-user-related.ts
+++ b/packages/backend/src/misc/is-user-related.ts
@@ -4,6 +4,10 @@
*/
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
+ if (!note) {
+ return false;
+ }
+
if (userIds.has(note.userId) && !ignoreAuthor) {
return true;
}
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 41e5bfe9e4..a721b8663c 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -4,12 +4,12 @@
*/
import {
- packedUserLiteSchema,
- packedUserDetailedNotMeOnlySchema,
packedMeDetailedOnlySchema,
- packedUserDetailedNotMeSchema,
packedMeDetailedSchema,
+ packedUserDetailedNotMeOnlySchema,
+ packedUserDetailedNotMeSchema,
packedUserDetailedSchema,
+ packedUserLiteSchema,
packedUserSchema,
} from '@/models/json-schema/user.js';
import { packedNoteSchema } from '@/models/json-schema/note.js';
@@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
-import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js';
+import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
import { packedChannelSchema } from '@/models/json-schema/channel.js';
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
@@ -38,25 +38,27 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js';
import {
- packedRoleLiteSchema,
- packedRoleSchema,
- packedRolePoliciesSchema,
+ packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
packedRoleCondFormulaLogicsSchema,
- packedRoleCondFormulaValueNot,
- packedRoleCondFormulaValueIsLocalOrRemoteSchema,
packedRoleCondFormulaValueAssignedRoleSchema,
packedRoleCondFormulaValueCreatedSchema,
- packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
+ packedRoleCondFormulaValueIsLocalOrRemoteSchema,
+ packedRoleCondFormulaValueNot,
packedRoleCondFormulaValueSchema,
packedRoleCondFormulaValueUserSettingBooleanSchema,
+ packedRoleLiteSchema,
+ packedRolePoliciesSchema,
+ packedRoleSchema,
} from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
-import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
+import { packedReversiGameDetailedSchema, packedReversiGameLiteSchema } from '@/models/json-schema/reversi-game.js';
import {
- packedMetaLiteSchema,
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
+ packedMetaLiteSchema,
} from '@/models/json-schema/meta.js';
+import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
+import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
export const refs = {
UserLite: packedUserLiteSchema,
@@ -111,6 +113,8 @@ export const refs = {
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
+ SystemWebhook: packedSystemWebhookSchema,
+ AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
};
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts
new file mode 100644
index 0000000000..7994441791
--- /dev/null
+++ b/packages/backend/src/misc/json-value.ts
@@ -0,0 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
+export type JsonObject = {[K in string]?: JsonValue};
+export type JsonArray = JsonValue[];
diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts
index dbfe1fff18..f741a0c913 100644
--- a/packages/backend/src/misc/prelude/array.ts
+++ b/packages/backend/src/misc/prelude/array.ts
@@ -66,44 +66,6 @@ export function maximum(xs: number[]): number {
}
/**
- * Splits an array based on the equivalence relation.
- * The concatenation of the result is equal to the argument.
- */
-export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
- const groups = [] as T[][];
- for (const x of xs) {
- const lastGroup = groups.at(-1);
- if (lastGroup !== undefined && f(lastGroup[0], x)) {
- lastGroup.push(x);
- } else {
- groups.push([x]);
- }
- }
- return groups;
-}
-
-/**
- * Splits an array based on the equivalence relation induced by the function.
- * The concatenation of the result is equal to the argument.
- */
-export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
- return groupBy((a, b) => f(a) === f(b), xs);
-}
-
-export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
- return collections.reduce((obj: Record<string, T[]>, item: T) => {
- const key = keySelector(item);
- if (!Object.prototype.hasOwnProperty.call(obj, key)) {
- obj[key] = [];
- }
-
- obj[key].push(item);
-
- return obj;
- }, {});
-}
-
-/**
* Compare two arrays by lexicographical order
*/
export function lessThan(xs: number[], ys: number[]): boolean {
diff --git a/packages/backend/src/misc/prelude/math.ts b/packages/backend/src/misc/prelude/math.ts
deleted file mode 100644
index 38556def2d..0000000000
--- a/packages/backend/src/misc/prelude/math.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function gcd(a: number, b: number): number {
- return b === 0 ? a : gcd(b, a % b);
-}
diff --git a/packages/backend/src/misc/prelude/maybe.ts b/packages/backend/src/misc/prelude/maybe.ts
deleted file mode 100644
index 1c58ccb9c7..0000000000
--- a/packages/backend/src/misc/prelude/maybe.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export interface IMaybe<T> {
- isJust(): this is IJust<T>;
-}
-
-export interface IJust<T> extends IMaybe<T> {
- get(): T;
-}
-
-export function just<T>(value: T): IJust<T> {
- return {
- isJust: () => true,
- get: () => value,
- };
-}
-
-export function nothing<T>(): IMaybe<T> {
- return {
- isJust: () => false,
- };
-}
diff --git a/packages/backend/src/misc/prelude/string.ts b/packages/backend/src/misc/prelude/string.ts
deleted file mode 100644
index 67ea529961..0000000000
--- a/packages/backend/src/misc/prelude/string.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function concat(xs: string[]): string {
- return xs.join('');
-}
-
-export function capitalize(s: string): string {
- return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
-}
-
-export function toUpperCase(s: string): string {
- return s.toUpperCase();
-}
-
-export function toLowerCase(s: string): string {
- return s.toLowerCase();
-}
diff --git a/packages/backend/src/misc/prelude/symbol.ts b/packages/backend/src/misc/prelude/symbol.ts
deleted file mode 100644
index 7e8d39bdb6..0000000000
--- a/packages/backend/src/misc/prelude/symbol.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export const fallback = Symbol('fallback');
diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
new file mode 100644
index 0000000000..fbff880afc
--- /dev/null
+++ b/packages/backend/src/models/AbuseReportNotificationRecipient.ts
@@ -0,0 +1,100 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
+import { MiUserProfile } from '@/models/UserProfile.js';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+
+/**
+ * 通報受信時に通知を送信する方法.
+ */
+export type RecipientMethod = 'email' | 'webhook';
+
+@Entity('abuse_report_notification_recipient')
+export class MiAbuseReportNotificationRecipient {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * 有効かどうか.
+ */
+ @Index()
+ @Column('boolean', {
+ default: true,
+ })
+ public isActive: boolean;
+
+ /**
+ * 更新日時.
+ */
+ @Column('timestamp with time zone', {
+ default: () => 'CURRENT_TIMESTAMP',
+ })
+ public updatedAt: Date;
+
+ /**
+ * 通知設定名.
+ */
+ @Column('varchar', {
+ length: 255,
+ })
+ public name: string;
+
+ /**
+ * 通知方法.
+ */
+ @Index()
+ @Column('varchar', {
+ length: 64,
+ })
+ public method: RecipientMethod;
+
+ /**
+ * 通知先のユーザID.
+ */
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public userId: MiUser['id'] | null;
+
+ /**
+ * 通知先のユーザ.
+ */
+ @ManyToOne(type => MiUser, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
+ public user: MiUser | null;
+
+ /**
+ * 通知先のユーザプロフィール.
+ */
+ @ManyToOne(type => MiUserProfile, {})
+ @JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
+ public userProfile: MiUserProfile | null;
+
+ /**
+ * 通知先のシステムWebhookId.
+ */
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public systemWebhookId: string | null;
+
+ /**
+ * 通知先のシステムWebhook.
+ */
+ @ManyToOne(type => MiSystemWebhook, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public systemWebhook: MiSystemWebhook | null;
+}
diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts
index 438b32f79a..7b03e3e494 100644
--- a/packages/backend/src/models/DriveFile.ts
+++ b/packages/backend/src/models/DriveFile.ts
@@ -82,7 +82,7 @@ export class MiDriveFile {
public storedInternal: boolean;
@Column('varchar', {
- length: 512,
+ length: 1024,
comment: 'The URL of the DriveFile.',
})
public url: string;
@@ -124,13 +124,13 @@ export class MiDriveFile {
@Index()
@Column('varchar', {
- length: 512, nullable: true,
+ length: 1024, nullable: true,
comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.',
})
public uri: string | null;
@Column('varchar', {
- length: 512, nullable: true,
+ length: 1024, nullable: true,
})
public src: string | null;
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index ad306fcad6..70d41801b5 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -87,6 +87,11 @@ export class MiMeta {
public silencedHosts: string[];
@Column('varchar', {
+ length: 1024, array: true, default: '{}',
+ })
+ public mediaSilencedHosts: string[];
+
+ @Column('varchar', {
length: 1024,
nullable: true,
})
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index d3062d6b36..ea0f88baba 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -3,11 +3,83 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { Provider } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { MiRepository, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame, miRepository } from './_.js';
+import {
+ MiAbuseReportNotificationRecipient,
+ MiAbuseUserReport,
+ MiAccessToken,
+ MiAd,
+ MiAnnouncement,
+ MiAnnouncementRead,
+ MiAntenna,
+ MiApp,
+ MiAuthSession,
+ MiAvatarDecoration,
+ MiBlocking,
+ MiBubbleGameRecord,
+ MiChannel,
+ MiChannelFavorite,
+ MiChannelFollowing,
+ MiClip,
+ MiClipFavorite,
+ MiClipNote,
+ MiDriveFile,
+ MiDriveFolder,
+ MiEmoji,
+ MiFlash,
+ MiFlashLike,
+ MiFollowing,
+ MiFollowRequest,
+ MiGalleryLike,
+ MiGalleryPost,
+ MiHashtag,
+ MiInstance,
+ MiMeta,
+ MiModerationLog,
+ MiMuting,
+ MiNote,
+ MiNoteFavorite,
+ MiNoteReaction,
+ MiNoteThreadMuting,
+ MiNoteUnread,
+ MiPage,
+ MiPageLike,
+ MiPasswordResetRequest,
+ MiPoll,
+ MiPollVote,
+ MiPromoNote,
+ MiPromoRead,
+ MiRegistrationTicket,
+ MiRegistryItem,
+ MiRelay,
+ MiRenoteMuting,
+ MiRepository,
+ miRepository,
+ MiRetentionAggregation,
+ MiReversiGame,
+ MiRole,
+ MiRoleAssignment,
+ MiSignin,
+ MiSwSubscription,
+ MiSystemWebhook,
+ MiUsedUsername,
+ MiUser,
+ MiUserIp,
+ MiUserKeypair,
+ MiUserList,
+ MiUserListFavorite,
+ MiUserListMembership,
+ MiUserMemo,
+ MiUserNotePining,
+ MiUserPending,
+ MiUserProfile,
+ MiUserPublickey,
+ MiUserSecurityKey,
+ MiWebhook
+} from './_.js';
import type { DataSource } from 'typeorm';
-import type { Provider } from '@nestjs/common';
const $usersRepository: Provider = {
provide: DI.usersRepository,
@@ -225,6 +297,12 @@ const $abuseUserReportsRepository: Provider = {
inject: [DI.db],
};
+const $abuseReportNotificationRecipientRepository: Provider = {
+ provide: DI.abuseReportNotificationRecipientRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
+ inject: [DI.db],
+};
+
const $registrationTicketsRepository: Provider = {
provide: DI.registrationTicketsRepository,
useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository<MiRegistrationTicket>),
@@ -351,6 +429,12 @@ const $webhooksRepository: Provider = {
inject: [DI.db],
};
+const $systemWebhooksRepository: Provider = {
+ provide: DI.systemWebhooksRepository,
+ useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
+ inject: [DI.db],
+};
+
const $adsRepository: Provider = {
provide: DI.adsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository<MiAd>),
@@ -412,8 +496,7 @@ const $reversiGamesRepository: Provider = {
};
@Module({
- imports: [
- ],
+ imports: [],
providers: [
$usersRepository,
$notesRepository,
@@ -451,6 +534,7 @@ const $reversiGamesRepository: Provider = {
$swSubscriptionsRepository,
$hashtagsRepository,
$abuseUserReportsRepository,
+ $abuseReportNotificationRecipientRepository,
$registrationTicketsRepository,
$authSessionsRepository,
$accessTokensRepository,
@@ -472,6 +556,7 @@ const $reversiGamesRepository: Provider = {
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
+ $systemWebhooksRepository,
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
@@ -520,6 +605,7 @@ const $reversiGamesRepository: Provider = {
$swSubscriptionsRepository,
$hashtagsRepository,
$abuseUserReportsRepository,
+ $abuseReportNotificationRecipientRepository,
$registrationTicketsRepository,
$authSessionsRepository,
$accessTokensRepository,
@@ -541,6 +627,7 @@ const $reversiGamesRepository: Provider = {
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
+ $systemWebhooksRepository,
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
@@ -553,4 +640,5 @@ const $reversiGamesRepository: Provider = {
$reversiGamesRepository,
],
})
-export class RepositoryModule {}
+export class RepositoryModule {
+}
diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts
new file mode 100644
index 0000000000..d6c27eae51
--- /dev/null
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -0,0 +1,100 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
+import { Serialized } from '@/types.js';
+import { id } from './util/id.js';
+
+export const systemWebhookEventTypes = [
+ // ユーザからの通報を受けたとき
+ 'abuseReport',
+ // 通報を処理したとき
+ 'abuseReportResolved',
+ // ユーザが作成された時
+ 'userCreated',
+] as const;
+export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
+
+@Entity('system_webhook')
+export class MiSystemWebhook {
+ @PrimaryColumn(id())
+ public id: string;
+
+ /**
+ * 有効かどうか.
+ */
+ @Index('IDX_system_webhook_isActive', { synchronize: false })
+ @Column('boolean', {
+ default: true,
+ })
+ public isActive: boolean;
+
+ /**
+ * 更新日時.
+ */
+ @Column('timestamp with time zone', {
+ default: () => 'CURRENT_TIMESTAMP',
+ })
+ public updatedAt: Date;
+
+ /**
+ * 最後に送信された日時.
+ */
+ @Column('timestamp with time zone', {
+ nullable: true,
+ })
+ public latestSentAt: Date | null;
+
+ /**
+ * 最後に送信されたステータスコード
+ */
+ @Column('integer', {
+ nullable: true,
+ })
+ public latestStatus: number | null;
+
+ /**
+ * 通知設定名.
+ */
+ @Column('varchar', {
+ length: 255,
+ })
+ public name: string;
+
+ /**
+ * イベント種別.
+ */
+ @Index('IDX_system_webhook_on', { synchronize: false })
+ @Column('varchar', {
+ length: 128,
+ array: true,
+ default: '{}',
+ })
+ public on: SystemWebhookEventType[];
+
+ /**
+ * Webhook送信先のURL.
+ */
+ @Column('varchar', {
+ length: 1024,
+ })
+ public url: string;
+
+ /**
+ * Webhook検証用の値.
+ */
+ @Column('varchar', {
+ length: 1024,
+ })
+ public secret: string;
+
+ static deserialize(obj: Serialized<MiSystemWebhook>): MiSystemWebhook {
+ return {
+ ...obj,
+ updatedAt: new Date(obj.updatedAt),
+ latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null,
+ };
+ }
+}
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index 2e6a41586e..c72bdaa727 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -11,6 +11,7 @@ import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transfor
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAccessToken } from '@/models/AccessToken.js';
import { MiAd } from '@/models/Ad.js';
import { MiAnnouncement } from '@/models/Announcement.js';
@@ -68,6 +69,7 @@ import { MiUserPublickey } from '@/models/UserPublickey.js';
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
import { MiUserMemo } from '@/models/UserMemo.js';
import { MiWebhook } from '@/models/Webhook.js';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
import { MiChannel } from '@/models/Channel.js';
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
import { MiRole } from '@/models/Role.js';
@@ -80,34 +82,14 @@ import { MiReversiGame } from '@/models/ReversiGame.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
export interface MiRepository<T extends ObjectLiteral> {
- createTableColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
- createTableColumnNamesWithPrimaryKey(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
+ createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
}
export const miRepository = {
- createTableColumnNames(queryBuilder) {
- // @ts-expect-error -- protected
- const insertedColumns = queryBuilder.getInsertedColumns();
- if (insertedColumns.length) {
- return insertedColumns.map(column => column.databaseName);
- }
- if (!queryBuilder.expressionMap.mainAlias?.hasMetadata && !queryBuilder.expressionMap.insertColumns.length) {
- // @ts-expect-error -- protected
- const valueSets = queryBuilder.getValueSets();
- if (valueSets.length === 1) {
- return Object.keys(valueSets[0]);
- }
- }
- return queryBuilder.expressionMap.insertColumns;
- },
- createTableColumnNamesWithPrimaryKey(queryBuilder) {
- const columnNames = this.createTableColumnNames(queryBuilder);
- if (!columnNames.includes('id')) {
- columnNames.unshift('id');
- }
- return columnNames;
+ createTableColumnNames() {
+ return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
},
async insertOne(entity, findOptions?) {
const queryBuilder = this.createQueryBuilder().insert().values(entity);
@@ -115,7 +97,7 @@ export const miRepository = {
const mainAlias = queryBuilder.expressionMap.mainAlias!;
const name = mainAlias.name;
mainAlias.name = 't';
- const columnNames = this.createTableColumnNamesWithPrimaryKey(queryBuilder);
+ const columnNames = this.createTableColumnNames();
queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -136,7 +118,7 @@ export const miRepository = {
selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
return builder.select(selection, selectionAliasName);
};
- for (const columnName of this.createTableColumnNamesWithPrimaryKey(queryBuilder)) {
+ for (const columnName of this.createTableColumnNames()) {
selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
}
},
@@ -144,6 +126,7 @@ export const miRepository = {
export {
MiAbuseUserReport,
+ MiAbuseReportNotificationRecipient,
MiAccessToken,
MiAd,
MiAnnouncement,
@@ -201,6 +184,7 @@ export {
MiUserPublickey,
MiUserSecurityKey,
MiWebhook,
+ MiSystemWebhook,
MiChannel,
MiRetentionAggregation,
MiRole,
@@ -213,6 +197,7 @@ export {
};
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
+export type AbuseReportNotificationRecipientRepository = Repository<MiAbuseReportNotificationRecipient> & MiRepository<MiAbuseReportNotificationRecipient>;
export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>;
export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
@@ -270,6 +255,7 @@ export type UserProfilesRepository = Repository<MiUserProfile> & MiRepository<Mi
export type UserPublickeysRepository = Repository<MiUserPublickey> & MiRepository<MiUserPublickey>;
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey> & MiRepository<MiUserSecurityKey>;
export type WebhooksRepository = Repository<MiWebhook> & MiRepository<MiWebhook>;
+export type SystemWebhooksRepository = Repository<MiSystemWebhook> & MiRepository<MiWebhook>;
export type ChannelsRepository = Repository<MiChannel> & MiRepository<MiChannel>;
export type RetentionAggregationsRepository = Repository<MiRetentionAggregation> & MiRepository<MiRetentionAggregation>;
export type RolesRepository = Repository<MiRole> & MiRepository<MiRole>;
diff --git a/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts
new file mode 100644
index 0000000000..6215f0f5a2
--- /dev/null
+++ b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const packedAbuseReportNotificationRecipientSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ isActive: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ updatedAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ method: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['email', 'webhook'],
+ },
+ userId: {
+ type: 'string',
+ optional: true, nullable: false,
+ },
+ user: {
+ type: 'object',
+ optional: true, nullable: false,
+ ref: 'UserLite',
+ },
+ systemWebhookId: {
+ type: 'string',
+ optional: true, nullable: false,
+ },
+ systemWebhook: {
+ type: 'object',
+ optional: true, nullable: false,
+ ref: 'SystemWebhook',
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts
index ca88cc0e39..5ee1561c50 100644
--- a/packages/backend/src/models/json-schema/drive-file.ts
+++ b/packages/backend/src/models/json-schema/drive-file.ts
@@ -20,7 +20,7 @@ export const packedDriveFileSchema = {
name: {
type: 'string',
optional: false, nullable: false,
- example: 'lenna.jpg',
+ example: '192.jpg',
},
type: {
type: 'string',
diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts
index ed40d405c6..912a0399d8 100644
--- a/packages/backend/src/models/json-schema/federation-instance.ts
+++ b/packages/backend/src/models/json-schema/federation-instance.ts
@@ -88,6 +88,10 @@ export const packedFederationInstanceSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ isMediaSilenced: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
iconUrl: {
type: 'string',
optional: false, nullable: true,
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index e7bc6356e5..3bcf9cac92 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -247,6 +247,12 @@ export const packedMetaLiteSchema = {
optional: false, nullable: false,
ref: 'RolePolicies',
},
+ noteSearchableScope: {
+ type: 'string',
+ enum: ['local', 'global'],
+ optional: false, nullable: false,
+ default: 'local',
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts
index 2641161c8b..432c096e48 100644
--- a/packages/backend/src/models/json-schema/note.ts
+++ b/packages/backend/src/models/json-schema/note.ts
@@ -204,6 +204,7 @@ export const packedNoteSchema = {
reactionAcceptance: {
type: 'string',
optional: false, nullable: true,
+ enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
},
reactionEmojis: {
type: 'object',
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index d9987a70c3..7366f05356 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ canUpdateBioMedia: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
pinLimit: {
type: 'integer',
optional: false, nullable: false,
diff --git a/packages/backend/src/models/json-schema/system-webhook.ts b/packages/backend/src/models/json-schema/system-webhook.ts
new file mode 100644
index 0000000000..d83065a743
--- /dev/null
+++ b/packages/backend/src/models/json-schema/system-webhook.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+
+export const packedSystemWebhookSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ isActive: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ updatedAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: false,
+ },
+ latestSentAt: {
+ type: 'string',
+ format: 'date-time',
+ optional: false, nullable: true,
+ },
+ latestStatus: {
+ type: 'number',
+ optional: false, nullable: true,
+ },
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ on: {
+ type: 'array',
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: systemWebhookEventTypes,
+ },
+ },
+ url: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ secret: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+} as const;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index 2d14537bbb..251a03c303 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -5,13 +5,12 @@
// https://github.com/typeorm/typeorm/issues/2400
import pg from 'pg';
-pg.types.setTypeParser(20, Number);
-
import { DataSource, Logger } from 'typeorm';
import * as highlight from 'cli-highlight';
import { entities as charts } from '@/core/chart/entities.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
import { MiAccessToken } from '@/models/AccessToken.js';
import { MiAd } from '@/models/Ad.js';
import { MiAnnouncement } from '@/models/Announcement.js';
@@ -69,6 +68,7 @@ import { MiUserProfile } from '@/models/UserProfile.js';
import { MiUserPublickey } from '@/models/UserPublickey.js';
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
import { MiWebhook } from '@/models/Webhook.js';
+import { MiSystemWebhook } from '@/models/SystemWebhook.js';
import { MiChannel } from '@/models/Channel.js';
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
import { MiRole } from '@/models/Role.js';
@@ -83,9 +83,11 @@ import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+pg.types.setTypeParser(20, Number);
+
export const dbLogger = new MisskeyLogger('db');
-const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
+const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
class MyCustomLogger implements Logger {
@bindThis
@@ -167,6 +169,7 @@ export const entities = [
MiHashtag,
MiSwSubscription,
MiAbuseUserReport,
+ MiAbuseReportNotificationRecipient,
MiRegistrationTicket,
MiSignin,
MiModerationLog,
@@ -185,6 +188,7 @@ export const entities = [
MiPasswordResetRequest,
MiUserPending,
MiWebhook,
+ MiSystemWebhook,
MiUserIp,
MiRetentionAggregation,
MiRole,
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index 8086158997..a1fd38fcc5 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -11,7 +11,8 @@ import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
-import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
+import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
+import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
@@ -71,7 +72,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeleteFileProcessorService,
CleanRemoteFilesProcessorService,
RelationshipProcessorService,
- WebhookDeliverProcessorService,
+ UserWebhookDeliverProcessorService,
+ SystemWebhookDeliverProcessorService,
EndedPollNotificationProcessorService,
DeliverProcessorService,
InboxProcessorService,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index ce999d9cef..7bd74f3210 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -5,11 +5,13 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
+import * as Sentry from '@sentry/node';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
-import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
+import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
+import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
@@ -75,7 +77,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
private dbQueueWorker: Bull.Worker;
private deliverQueueWorker: Bull.Worker;
private inboxQueueWorker: Bull.Worker;
- private webhookDeliverQueueWorker: Bull.Worker;
+ private userWebhookDeliverQueueWorker: Bull.Worker;
+ private systemWebhookDeliverQueueWorker: Bull.Worker;
private relationshipQueueWorker: Bull.Worker;
private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker;
@@ -85,7 +88,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
private config: Config,
private queueLoggerService: QueueLoggerService,
- private webhookDeliverProcessorService: WebhookDeliverProcessorService,
+ private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
+ private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
private deliverProcessorService: DeliverProcessorService,
private inboxProcessorService: InboxProcessorService,
@@ -135,199 +139,367 @@ export class QueueProcessorService implements OnApplicationShutdown {
}
//#region system
- this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
- switch (job.name) {
- case 'tickCharts': return this.tickChartsProcessorService.process();
- case 'resyncCharts': return this.resyncChartsProcessorService.process();
- case 'cleanCharts': return this.cleanChartsProcessorService.process();
- case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
- case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
- case 'clean': return this.cleanProcessorService.process();
- default: throw new Error(`unrecognized job type ${job.name} for system`);
- }
- }, {
- ...baseQueueOptions(this.config, QUEUE.SYSTEM),
- autorun: false,
- });
+ {
+ const processer = (job: Bull.Job) => {
+ switch (job.name) {
+ case 'tickCharts': return this.tickChartsProcessorService.process();
+ case 'resyncCharts': return this.resyncChartsProcessorService.process();
+ case 'cleanCharts': return this.cleanChartsProcessorService.process();
+ case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
+ case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
+ case 'clean': return this.cleanProcessorService.process();
+ default: throw new Error(`unrecognized job type ${job.name} for system`);
+ }
+ };
+
+ this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
+ } else {
+ return processer(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.SYSTEM),
+ autorun: false,
+ });
- const systemLogger = this.logger.createSubLogger('system');
+ const logger = this.logger.createSubLogger('system');
- this.systemQueueWorker
- .on('active', (job) => systemLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
- .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
+ this.systemQueueWorker
+ .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) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
+ level: 'error',
+ extra: { job, err },
+ });
+ }
+ })
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region db
- this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
- switch (job.name) {
- case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
- case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
- case 'exportNotes': return this.exportNotesProcessorService.process(job);
- case 'exportClips': return this.exportClipsProcessorService.process(job);
- case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
- case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
- case 'exportMuting': return this.exportMutingProcessorService.process(job);
- case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
- case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
- case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
- case 'importFollowing': return this.importFollowingProcessorService.process(job);
- case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
- case 'importMuting': return this.importMutingProcessorService.process(job);
- case 'importBlocking': return this.importBlockingProcessorService.process(job);
- case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
- case 'importUserLists': return this.importUserListsProcessorService.process(job);
- case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
- case 'importAntennas': return this.importAntennasProcessorService.process(job);
- case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
- default: throw new Error(`unrecognized job type ${job.name} for db`);
- }
- }, {
- ...baseQueueOptions(this.config, QUEUE.DB),
- autorun: false,
- });
+ {
+ const processer = (job: Bull.Job) => {
+ switch (job.name) {
+ case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
+ case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
+ case 'exportNotes': return this.exportNotesProcessorService.process(job);
+ case 'exportClips': return this.exportClipsProcessorService.process(job);
+ case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
+ case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
+ case 'exportMuting': return this.exportMutingProcessorService.process(job);
+ case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
+ case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
+ case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
+ case 'importFollowing': return this.importFollowingProcessorService.process(job);
+ case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
+ case 'importMuting': return this.importMutingProcessorService.process(job);
+ case 'importBlocking': return this.importBlockingProcessorService.process(job);
+ case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
+ case 'importUserLists': return this.importUserListsProcessorService.process(job);
+ case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
+ case 'importAntennas': return this.importAntennasProcessorService.process(job);
+ case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
+ default: throw new Error(`unrecognized job type ${job.name} for db`);
+ }
+ };
+
+ this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
+ } else {
+ return processer(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.DB),
+ autorun: false,
+ });
- const dbLogger = this.logger.createSubLogger('db');
+ const logger = this.logger.createSubLogger('db');
- this.dbQueueWorker
- .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
- .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
+ this.dbQueueWorker
+ .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) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
+ level: 'error',
+ extra: { job, err },
+ });
+ }
+ })
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region deliver
- this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), {
- ...baseQueueOptions(this.config, QUEUE.DELIVER),
- autorun: false,
- concurrency: this.config.deliverJobConcurrency ?? 128,
- limiter: {
- max: this.config.deliverJobPerSec ?? 128,
- duration: 1000,
- },
- settings: {
- backoffStrategy: httpRelatedBackoff,
- },
- });
+ {
+ this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
+ } else {
+ return this.deliverProcessorService.process(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.DELIVER),
+ autorun: false,
+ concurrency: this.config.deliverJobConcurrency ?? 128,
+ limiter: {
+ max: this.config.deliverJobPerSec ?? 128,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
- const deliverLogger = this.logger.createSubLogger('deliver');
+ const logger = this.logger.createSubLogger('deliver');
- this.deliverQueueWorker
- .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
- .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
- .on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
- .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
+ this.deliverQueueWorker
+ .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 : '-'}`);
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
+ level: 'error',
+ extra: { job, err },
+ });
+ }
+ })
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region inbox
- this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
- ...baseQueueOptions(this.config, QUEUE.INBOX),
- autorun: false,
- concurrency: this.config.inboxJobConcurrency ?? 16,
- limiter: {
- max: this.config.inboxJobPerSec ?? 32,
- duration: 1000,
- },
- settings: {
- backoffStrategy: httpRelatedBackoff,
- },
- });
+ {
+ this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
+ } else {
+ return this.inboxProcessorService.process(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.INBOX),
+ autorun: false,
+ concurrency: this.config.inboxJobConcurrency ?? 16,
+ limiter: {
+ max: this.config.inboxJobPerSec ?? 32,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
- const inboxLogger = this.logger.createSubLogger('inbox');
+ const logger = this.logger.createSubLogger('inbox');
- this.inboxQueueWorker
- .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
- .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
- .on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
- .on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
+ this.inboxQueueWorker
+ .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) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
+ level: 'error',
+ extra: { job, err },
+ });
+ }
+ })
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+ }
//#endregion
- //#region webhook deliver
- this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), {
- ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
- autorun: false,
- concurrency: 64,
- limiter: {
- max: 64,
- duration: 1000,
- },
- settings: {
- backoffStrategy: httpRelatedBackoff,
- },
- });
+ //#region user-webhook deliver
+ {
+ this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
+ } else {
+ return this.userWebhookDeliverProcessorService.process(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
+ autorun: false,
+ concurrency: 64,
+ limiter: {
+ max: 64,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
- const webhookLogger = this.logger.createSubLogger('webhook');
+ const logger = this.logger.createSubLogger('user-webhook');
- this.webhookDeliverQueueWorker
- .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
- .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
- .on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
- .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
+ this.userWebhookDeliverQueueWorker
+ .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 : '-'}`);
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, {
+ level: 'error',
+ extra: { job, err },
+ });
+ }
+ })
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+ }
+ //#endregion
+
+ //#region system-webhook deliver
+ {
+ this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job));
+ } else {
+ return this.systemWebhookDeliverProcessorService.process(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
+ autorun: false,
+ concurrency: 16,
+ limiter: {
+ max: 16,
+ duration: 1000,
+ },
+ settings: {
+ backoffStrategy: httpRelatedBackoff,
+ },
+ });
+
+ const logger = this.logger.createSubLogger('system-webhook');
+
+ this.systemWebhookDeliverQueueWorker
+ .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 : '-'}`);
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, {
+ level: 'error',
+ extra: { job, err },
+ });
+ }
+ })
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region relationship
- this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
- switch (job.name) {
- case 'follow': return this.relationshipProcessorService.processFollow(job);
- case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
- case 'block': return this.relationshipProcessorService.processBlock(job);
- case 'unblock': return this.relationshipProcessorService.processUnblock(job);
- default: throw new Error(`unrecognized job type ${job.name} for relationship`);
- }
- }, {
- ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
- autorun: false,
- concurrency: this.config.relationshipJobConcurrency ?? 16,
- limiter: {
- max: this.config.relationshipJobPerSec ?? 64,
- duration: 1000,
- },
- });
+ {
+ const processer = (job: Bull.Job) => {
+ switch (job.name) {
+ case 'follow': return this.relationshipProcessorService.processFollow(job);
+ case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
+ case 'block': return this.relationshipProcessorService.processBlock(job);
+ case 'unblock': return this.relationshipProcessorService.processUnblock(job);
+ default: throw new Error(`unrecognized job type ${job.name} for relationship`);
+ }
+ };
+
+ this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
+ } else {
+ return processer(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
+ autorun: false,
+ concurrency: this.config.relationshipJobConcurrency ?? 16,
+ limiter: {
+ max: this.config.relationshipJobPerSec ?? 64,
+ duration: 1000,
+ },
+ });
- const relationshipLogger = this.logger.createSubLogger('relationship');
+ const logger = this.logger.createSubLogger('relationship');
- this.relationshipQueueWorker
- .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
- .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
+ this.relationshipQueueWorker
+ .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) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
+ level: 'error',
+ extra: { job, err },
+ });
+ }
+ })
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region object storage
- this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
- switch (job.name) {
- case 'deleteFile': return this.deleteFileProcessorService.process(job);
- case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
- default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
- }
- }, {
- ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
- autorun: false,
- concurrency: 16,
- });
+ {
+ const processer = (job: Bull.Job) => {
+ switch (job.name) {
+ case 'deleteFile': return this.deleteFileProcessorService.process(job);
+ case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
+ default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
+ }
+ };
+
+ this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
+ } else {
+ return processer(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
+ autorun: false,
+ concurrency: 16,
+ });
- const objectStorageLogger = this.logger.createSubLogger('objectStorage');
+ const logger = this.logger.createSubLogger('objectStorage');
- this.objectStorageQueueWorker
- .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
- .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
- .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
- .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
- .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
+ this.objectStorageQueueWorker
+ .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) });
+ if (config.sentryForBackend) {
+ Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
+ level: 'error',
+ extra: { job, err },
+ });
+ }
+ })
+ .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
+ .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
+ }
//#endregion
//#region ended poll notification
- this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), {
- ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
- autorun: false,
- });
+ {
+ this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
+ if (this.config.sentryForBackend) {
+ return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
+ } else {
+ return this.endedPollNotificationProcessorService.process(job);
+ }
+ }, {
+ ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
+ autorun: false,
+ });
+ }
//#endregion
}
@@ -338,7 +510,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker.run(),
this.deliverQueueWorker.run(),
this.inboxQueueWorker.run(),
- this.webhookDeliverQueueWorker.run(),
+ this.userWebhookDeliverQueueWorker.run(),
+ this.systemWebhookDeliverQueueWorker.run(),
this.relationshipQueueWorker.run(),
this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(),
@@ -352,7 +525,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker.close(),
this.deliverQueueWorker.close(),
this.inboxQueueWorker.close(),
- this.webhookDeliverQueueWorker.close(),
+ this.userWebhookDeliverQueueWorker.close(),
+ this.systemWebhookDeliverQueueWorker.close(),
this.relationshipQueueWorker.close(),
this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(),
diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts
index 132e916612..67f689b618 100644
--- a/packages/backend/src/queue/const.ts
+++ b/packages/backend/src/queue/const.ts
@@ -14,7 +14,8 @@ export const QUEUE = {
DB: 'db',
RELATIONSHIP: 'relationship',
OBJECT_STORAGE: 'objectStorage',
- WEBHOOK_DELIVER: 'webhookDeliver',
+ USER_WEBHOOK_DELIVER: 'userWebhookDeliver',
+ SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver',
};
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index b73195afc3..d665945861 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -109,6 +109,12 @@ export class DeliverProcessorService {
suspensionState: 'autoSuspendedForNotResponding',
});
}
+ } else {
+ // isNotRespondingがtrueでnotRespondingSinceがnullの場合はnotRespondingSinceをセット
+ // notRespondingSinceは新たな機能なので、それ以前のデータにはnotRespondingSinceがない場合がある
+ this.federatedInstanceService.update(i.id, {
+ notRespondingSince: new Date(),
+ });
}
this.apRequestChart.deliverFail();
diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts
index 29c1f27bb1..34180e5f2b 100644
--- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts
+++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { PollVotesRepository, NotesRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
+import { CacheService } from '@/core/CacheService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
@@ -24,6 +25,7 @@ export class EndedPollNotificationProcessorService {
@Inject(DI.pollVotesRepository)
private pollVotesRepository: PollVotesRepository,
+ private cacheService: CacheService,
private notificationService: NotificationService,
private queueLoggerService: QueueLoggerService,
) {
@@ -47,9 +49,12 @@ export class EndedPollNotificationProcessorService {
const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])];
for (const userId of userIds) {
- this.notificationService.createNotification(userId, 'pollEnded', {
- noteId: note.id,
- });
+ const profile = await this.cacheService.userProfileCache.fetch(userId);
+ if (profile.userHost === null) {
+ this.notificationService.createNotification(userId, 'pollEnded', {
+ noteId: note.id,
+ });
+ }
}
}
}
diff --git a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts
new file mode 100644
index 0000000000..f6bef52684
--- /dev/null
+++ b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts
@@ -0,0 +1,87 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Bull from 'bullmq';
+import { DI } from '@/di-symbols.js';
+import type { SystemWebhooksRepository } from '@/models/_.js';
+import type { Config } from '@/config.js';
+import type Logger from '@/logger.js';
+import { HttpRequestService } from '@/core/HttpRequestService.js';
+import { StatusError } from '@/misc/status-error.js';
+import { bindThis } from '@/decorators.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import { SystemWebhookDeliverJobData } from '../types.js';
+
+@Injectable()
+export class SystemWebhookDeliverProcessorService {
+ private logger: Logger;
+
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.systemWebhooksRepository)
+ private systemWebhooksRepository: SystemWebhooksRepository,
+
+ private httpRequestService: HttpRequestService,
+ private queueLoggerService: QueueLoggerService,
+ ) {
+ this.logger = this.queueLoggerService.logger.createSubLogger('webhook');
+ }
+
+ @bindThis
+ public async process(job: Bull.Job<SystemWebhookDeliverJobData>): Promise<string> {
+ try {
+ this.logger.debug(`delivering ${job.data.webhookId}`);
+
+ const res = await this.httpRequestService.send(job.data.to, {
+ method: 'POST',
+ headers: {
+ 'User-Agent': 'Misskey-Hooks',
+ 'X-Misskey-Host': this.config.host,
+ 'X-Misskey-Hook-Id': job.data.webhookId,
+ 'X-Misskey-Hook-Secret': job.data.secret,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ server: this.config.url,
+ hookId: job.data.webhookId,
+ eventId: job.data.eventId,
+ createdAt: job.data.createdAt,
+ type: job.data.type,
+ body: job.data.content,
+ }),
+ });
+
+ this.systemWebhooksRepository.update({ id: job.data.webhookId }, {
+ latestSentAt: new Date(),
+ latestStatus: res.status,
+ });
+
+ return 'Success';
+ } catch (res) {
+ this.logger.error(res as Error);
+
+ this.systemWebhooksRepository.update({ id: job.data.webhookId }, {
+ latestSentAt: new Date(),
+ latestStatus: res instanceof StatusError ? res.statusCode : 1,
+ });
+
+ if (res instanceof StatusError) {
+ // 4xx
+ if (!res.isRetryable) {
+ throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
+ }
+
+ // 5xx etc.
+ throw new Error(`${res.statusCode} ${res.statusMessage}`);
+ } else {
+ // DNS error, socket error, timeout ...
+ throw res;
+ }
+ }
+ }
+}
diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts
index 8c260c0137..9ec630ef70 100644
--- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts
@@ -13,10 +13,10 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
-import type { WebhookDeliverJobData } from '../types.js';
+import { UserWebhookDeliverJobData } from '../types.js';
@Injectable()
-export class WebhookDeliverProcessorService {
+export class UserWebhookDeliverProcessorService {
private logger: Logger;
constructor(
@@ -33,7 +33,7 @@ export class WebhookDeliverProcessorService {
}
@bindThis
- public async process(job: Bull.Job<WebhookDeliverJobData>): Promise<string> {
+ public async process(job: Bull.Job<UserWebhookDeliverJobData>): Promise<string> {
try {
this.logger.debug(`delivering ${job.data.webhookId}`);
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index ce57ba745e..a4077a0547 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -106,7 +106,17 @@ export type EndedPollNotificationJobData = {
noteId: MiNote['id'];
};
-export type WebhookDeliverJobData = {
+export type SystemWebhookDeliverJobData = {
+ type: string;
+ content: unknown;
+ webhookId: MiWebhook['id'];
+ to: string;
+ secret: string;
+ createdAt: number;
+ eventId: string;
+};
+
+export type UserWebhookDeliverJobData = {
type: string;
content: unknown;
webhookId: MiWebhook['id'];
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 9db3aa1bfb..77a637d895 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -53,7 +53,7 @@ export class FileServerService {
private internalStorageService: InternalStorageService,
private loggerService: LoggerService,
) {
- this.logger = this.loggerService.getLogger('server', 'gray', false);
+ this.logger = this.loggerService.getLogger('server', 'gray');
//this.createServer = this.createServer.bind(this);
}
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 3572f16627..9c849480f2 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -68,7 +68,7 @@ export class ServerService implements OnApplicationShutdown {
private loggerService: LoggerService,
private oauth2ProviderService: OAuth2ProviderService,
) {
- this.logger = this.loggerService.getLogger('server', 'gray', false);
+ this.logger = this.loggerService.getLogger('server', 'gray');
}
@bindThis
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 271ef80554..47f64f6609 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -73,6 +73,16 @@ export class ApiCallService implements OnApplicationShutdown {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
}
statusCode = statusCode ?? 403;
+ } else if (err.code === 'RATE_LIMIT_EXCEEDED') {
+ const info: unknown = err.info;
+ const unixEpochInSeconds = Date.now();
+ if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') {
+ const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000);
+ // もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
+ reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
+ } else {
+ this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
+ }
} else if (!statusCode) {
statusCode = 500;
}
@@ -93,7 +103,7 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
- #onExecError(ep: IEndpoint, data: any, err: Error): void {
+ #onExecError(ep: IEndpoint, data: any, err: Error, userId?: MiUser['id']): void {
if (err instanceof ApiError || err instanceof AuthenticationError) {
throw err;
} else {
@@ -108,10 +118,13 @@ export class ApiCallService implements OnApplicationShutdown {
id: errId,
},
});
- console.error(err, errId);
if (this.config.sentryForBackend) {
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ level: 'error',
+ user: {
+ id: userId,
+ },
extra: {
ep: ep.name,
ps: data,
@@ -305,12 +318,17 @@ export class ApiCallService implements OnApplicationShutdown {
if (factor > 0) {
// Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
- throw new ApiError({
- message: 'Rate limit exceeded. Please try again later.',
- code: 'RATE_LIMIT_EXCEEDED',
- id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
- httpStatusCode: 429,
- });
+ if ('info' in err) {
+ // errはLimiter.LimiterInfoであることが期待される
+ throw new ApiError({
+ message: 'Rate limit exceeded. Please try again later.',
+ code: 'RATE_LIMIT_EXCEEDED',
+ id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+ httpStatusCode: 429,
+ }, err.info);
+ } else {
+ throw new TypeError('information must be a rate-limiter information.');
+ }
});
}
}
@@ -410,9 +428,13 @@ export class ApiCallService implements OnApplicationShutdown {
// API invoking
if (this.config.sentryForBackend) {
- return await Sentry.startSpan({ name: 'API: ' + ep.name }, () => ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err)));
+ return await Sentry.startSpan({
+ name: 'API: ' + ep.name,
+ }, () => ep.exec(data, user, token, file, request.ip, request.headers)
+ .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
} else {
- return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err));
+ return await ep.exec(data, user, token, file, request.ip, request.headers)
+ .catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
}
}
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index c645f4bcc6..41576bedaa 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -6,8 +6,13 @@
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
-import * as ep___admin_meta from './endpoints/admin/meta.js';
+import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
+import * as ep___admin_abuseReport_notificationRecipient_show from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
+import * as ep___admin_abuseReport_notificationRecipient_create from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
+import * as ep___admin_abuseReport_notificationRecipient_update from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
+import * as ep___admin_abuseReport_notificationRecipient_delete from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
+import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
@@ -82,6 +87,11 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
+import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
+import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
+import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
+import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
+import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@@ -381,6 +391,11 @@ import type { Provider } from '@nestjs/common';
const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
+const $admin_abuseReport_notificationRecipient_list: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/list', useClass: ep___admin_abuseReport_notificationRecipient_list.default };
+const $admin_abuseReport_notificationRecipient_show: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/show', useClass: ep___admin_abuseReport_notificationRecipient_show.default };
+const $admin_abuseReport_notificationRecipient_create: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/create', useClass: ep___admin_abuseReport_notificationRecipient_create.default };
+const $admin_abuseReport_notificationRecipient_update: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/update', useClass: ep___admin_abuseReport_notificationRecipient_update.default };
+const $admin_abuseReport_notificationRecipient_delete: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/delete', useClass: ep___admin_abuseReport_notificationRecipient_delete.default };
const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default };
const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default };
@@ -455,6 +470,11 @@ const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useCla
const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
+const $admin_systemWebhook_create: Provider = { provide: 'ep:admin/system-webhook/create', useClass: ep___admin_systemWebhook_create.default };
+const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhook/delete', useClass: ep___admin_systemWebhook_delete.default };
+const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
+const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
+const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
@@ -758,6 +778,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
ApiLoggerService,
$admin_meta,
$admin_abuseUserReports,
+ $admin_abuseReport_notificationRecipient_list,
+ $admin_abuseReport_notificationRecipient_show,
+ $admin_abuseReport_notificationRecipient_create,
+ $admin_abuseReport_notificationRecipient_update,
+ $admin_abuseReport_notificationRecipient_delete,
$admin_accounts_create,
$admin_accounts_delete,
$admin_accounts_findByEmail,
@@ -832,6 +857,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
+ $admin_systemWebhook_create,
+ $admin_systemWebhook_delete,
+ $admin_systemWebhook_list,
+ $admin_systemWebhook_show,
+ $admin_systemWebhook_update,
$announcements,
$announcements_show,
$antennas_create,
@@ -1129,6 +1159,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
exports: [
$admin_meta,
$admin_abuseUserReports,
+ $admin_abuseReport_notificationRecipient_list,
+ $admin_abuseReport_notificationRecipient_show,
+ $admin_abuseReport_notificationRecipient_create,
+ $admin_abuseReport_notificationRecipient_update,
+ $admin_abuseReport_notificationRecipient_delete,
$admin_accounts_create,
$admin_accounts_delete,
$admin_accounts_findByEmail,
@@ -1203,6 +1238,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
+ $admin_systemWebhook_create,
+ $admin_systemWebhook_delete,
+ $admin_systemWebhook_list,
+ $admin_systemWebhook_show,
+ $admin_systemWebhook_update,
$announcements,
$announcements_show,
$antennas_create,
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index 0439cdfe5e..52d73baa0a 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -32,11 +32,13 @@ export class RateLimiterService {
@bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
- return new Promise<void>((ok, reject) => {
- if (this.disabled) ok();
+ {
+ if (this.disabled) {
+ return Promise.resolve();
+ }
// Short-term limit
- const min = (): void => {
+ const min = new Promise<void>((ok, reject) => {
const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval! * factor,
@@ -46,25 +48,25 @@ export class RateLimiterService {
minIntervalLimiter.get((err, info) => {
if (err) {
- return reject('ERR');
+ return reject({ code: 'ERR', info });
}
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) {
- reject('BRIEF_REQUEST_INTERVAL');
+ return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
} else {
if (hasLongTermLimit) {
- max();
+ return max.then(ok, reject);
} else {
- ok();
+ return ok();
}
}
});
- };
+ });
// Long term limit
- const max = (): void => {
+ const max = new Promise<void>((ok, reject) => {
const limiter = new Limiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration! * factor,
@@ -74,18 +76,18 @@ export class RateLimiterService {
limiter.get((err, info) => {
if (err) {
- return reject('ERR');
+ return reject({ code: 'ERR', info });
}
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) {
- reject('RATE_LIMIT_EXCEEDED');
+ return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
} else {
- ok();
+ return ok();
}
});
- };
+ });
const hasShortTermLimit = typeof limitation.minInterval === 'number';
@@ -94,12 +96,12 @@ export class RateLimiterService {
typeof limitation.max === 'number';
if (hasShortTermLimit) {
- min();
+ return min;
} else if (hasLongTermLimit) {
- max();
+ return max;
} else {
- ok();
+ return Promise.resolve();
}
- });
+ }
}
}
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index a38c62f35a..3dfb7fdad4 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -6,8 +6,18 @@
import { permissions } from 'misskey-js';
import type { KeyOf, Schema } from '@/misc/json-schema.js';
-import * as ep___admin_meta from './endpoints/admin/meta.js';
+import * as ep___admin_abuseReport_notificationRecipient_list
+ from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
+import * as ep___admin_abuseReport_notificationRecipient_show
+ from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
+import * as ep___admin_abuseReport_notificationRecipient_create
+ from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
+import * as ep___admin_abuseReport_notificationRecipient_update
+ from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
+import * as ep___admin_abuseReport_notificationRecipient_delete
+ from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
+import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
@@ -44,7 +54,8 @@ import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-c
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
-import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
+import * as ep___admin_federation_refreshRemoteInstanceMetadata
+ from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
@@ -82,6 +93,11 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
+import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
+import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
+import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
+import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
+import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@@ -379,6 +395,11 @@ import * as ep___reversi_verify from './endpoints/reversi/verify.js';
const eps = [
['admin/meta', ep___admin_meta],
['admin/abuse-user-reports', ep___admin_abuseUserReports],
+ ['admin/abuse-report/notification-recipient/list', ep___admin_abuseReport_notificationRecipient_list],
+ ['admin/abuse-report/notification-recipient/show', ep___admin_abuseReport_notificationRecipient_show],
+ ['admin/abuse-report/notification-recipient/create', ep___admin_abuseReport_notificationRecipient_create],
+ ['admin/abuse-report/notification-recipient/update', ep___admin_abuseReport_notificationRecipient_update],
+ ['admin/abuse-report/notification-recipient/delete', ep___admin_abuseReport_notificationRecipient_delete],
['admin/accounts/create', ep___admin_accounts_create],
['admin/accounts/delete', ep___admin_accounts_delete],
['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
@@ -453,6 +474,11 @@ const eps = [
['admin/roles/unassign', ep___admin_roles_unassign],
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
['admin/roles/users', ep___admin_roles_users],
+ ['admin/system-webhook/create', ep___admin_systemWebhook_create],
+ ['admin/system-webhook/delete', ep___admin_systemWebhook_delete],
+ ['admin/system-webhook/list', ep___admin_systemWebhook_list],
+ ['admin/system-webhook/show', ep___admin_systemWebhook_show],
+ ['admin/system-webhook/update', ep___admin_systemWebhook_update],
['announcements', ep___announcements],
['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
@@ -873,8 +899,12 @@ export interface IEndpoint {
const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
return {
name: name,
- get meta() { return ep.meta ?? {}; },
- get params() { return ep.paramDef; },
+ get meta() {
+ return ep.meta ?? {};
+ },
+ get params() {
+ return ep.paramDef;
+ },
};
});
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts
new file mode 100644
index 0000000000..bdfbcba518
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts
@@ -0,0 +1,122 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ApiError } from '@/server/api/error.js';
+import {
+ AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { DI } from '@/di-symbols.js';
+import type { UserProfilesRepository } from '@/models/_.js';
+
+export const meta = {
+ tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'write:admin:abuse-report:notification-recipient',
+
+ res: {
+ type: 'object',
+ ref: 'AbuseReportNotificationRecipient',
+ },
+
+ errors: {
+ correlationCheckEmail: {
+ message: 'If "method" is email, "userId" must be set.',
+ code: 'CORRELATION_CHECK_EMAIL',
+ id: '348bb8ae-575a-6fe9-4327-5811999def8f',
+ httpStatusCode: 400,
+ },
+ correlationCheckWebhook: {
+ message: 'If "method" is webhook, "systemWebhookId" must be set.',
+ code: 'CORRELATION_CHECK_WEBHOOK',
+ id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
+ httpStatusCode: 400,
+ },
+ emailAddressNotSet: {
+ message: 'Email address is not set.',
+ code: 'EMAIL_ADDRESS_NOT_SET',
+ id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
+ httpStatusCode: 400,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ isActive: {
+ type: 'boolean',
+ },
+ name: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 255,
+ },
+ method: {
+ type: 'string',
+ enum: ['email', 'webhook'],
+ },
+ userId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ systemWebhookId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ },
+ required: [
+ 'isActive',
+ 'name',
+ 'method',
+ ],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ if (ps.method === 'email') {
+ const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
+ if (!ps.userId || !userProfile) {
+ throw new ApiError(meta.errors.correlationCheckEmail);
+ }
+
+ if (!userProfile.email || !userProfile.emailVerified) {
+ throw new ApiError(meta.errors.emailAddressNotSet);
+ }
+ }
+
+ if (ps.method === 'webhook' && !ps.systemWebhookId) {
+ throw new ApiError(meta.errors.correlationCheckWebhook);
+ }
+
+ const userId = ps.method === 'email' ? ps.userId : null;
+ const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
+ const result = await this.abuseReportNotificationService.createRecipient(
+ {
+ isActive: ps.isActive,
+ name: ps.name,
+ method: ps.method,
+ userId: userId ?? null,
+ systemWebhookId: systemWebhookId ?? null,
+ },
+ me,
+ );
+
+ return this.abuseReportNotificationRecipientEntityService.pack(result);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts
new file mode 100644
index 0000000000..b6dc44e09c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+
+export const meta = {
+ tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'write:admin:abuse-report:notification-recipient',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ },
+ required: [
+ 'id',
+ ],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.abuseReportNotificationService.deleteRecipient(
+ ps.id,
+ me,
+ );
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts
new file mode 100644
index 0000000000..dad9161a8a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import {
+ AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+
+export const meta = {
+ tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'read:admin:abuse-report:notification-recipient',
+
+ res: {
+ type: 'array',
+ items: {
+ type: 'object',
+ ref: 'AbuseReportNotificationRecipient',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ method: {
+ type: 'array',
+ items: {
+ type: 'string',
+ enum: ['email', 'webhook'],
+ },
+ },
+ },
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const recipients = await this.abuseReportNotificationService.fetchRecipients({ method: ps.method });
+ return this.abuseReportNotificationRecipientEntityService.packMany(recipients);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts
new file mode 100644
index 0000000000..557798f946
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import {
+ AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'read:admin:abuse-report:notification-recipient',
+
+ res: {
+ type: 'object',
+ ref: 'AbuseReportNotificationRecipient',
+ },
+
+ errors: {
+ noSuchRecipient: {
+ message: 'No such recipient.',
+ code: 'NO_SUCH_RECIPIENT',
+ id: '013de6a8-f757-04cb-4d73-cc2a7e3368e4',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ },
+ required: ['id'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const recipients = await this.abuseReportNotificationService.fetchRecipients({ ids: [ps.id] });
+ if (recipients.length === 0) {
+ throw new ApiError(meta.errors.noSuchRecipient);
+ }
+
+ return this.abuseReportNotificationRecipientEntityService.pack(recipients[0]);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts
new file mode 100644
index 0000000000..bd4b485217
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts
@@ -0,0 +1,128 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ApiError } from '@/server/api/error.js';
+import {
+ AbuseReportNotificationRecipientEntityService,
+} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
+import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
+import { DI } from '@/di-symbols.js';
+import type { UserProfilesRepository } from '@/models/_.js';
+
+export const meta = {
+ tags: ['admin', 'abuse-report', 'notification-recipient'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'write:admin:abuse-report:notification-recipient',
+
+ res: {
+ type: 'object',
+ ref: 'AbuseReportNotificationRecipient',
+ },
+
+ errors: {
+ correlationCheckEmail: {
+ message: 'If "method" is email, "userId" must be set.',
+ code: 'CORRELATION_CHECK_EMAIL',
+ id: '348bb8ae-575a-6fe9-4327-5811999def8f',
+ httpStatusCode: 400,
+ },
+ correlationCheckWebhook: {
+ message: 'If "method" is webhook, "systemWebhookId" must be set.',
+ code: 'CORRELATION_CHECK_WEBHOOK',
+ id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
+ httpStatusCode: 400,
+ },
+ emailAddressNotSet: {
+ message: 'Email address is not set.',
+ code: 'EMAIL_ADDRESS_NOT_SET',
+ id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
+ httpStatusCode: 400,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ isActive: {
+ type: 'boolean',
+ },
+ name: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 255,
+ },
+ method: {
+ type: 'string',
+ enum: ['email', 'webhook'],
+ },
+ userId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ systemWebhookId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ },
+ required: [
+ 'id',
+ 'isActive',
+ 'name',
+ 'method',
+ ],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+ private abuseReportNotificationService: AbuseReportNotificationService,
+ private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ if (ps.method === 'email') {
+ const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
+ if (!ps.userId || !userProfile) {
+ throw new ApiError(meta.errors.correlationCheckEmail);
+ }
+
+ if (!userProfile.email || !userProfile.emailVerified) {
+ throw new ApiError(meta.errors.emailAddressNotSet);
+ }
+ }
+
+ if (ps.method === 'webhook' && !ps.systemWebhookId) {
+ throw new ApiError(meta.errors.correlationCheckWebhook);
+ }
+
+ const userId = ps.method === 'email' ? ps.userId : null;
+ const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
+ const result = await this.abuseReportNotificationService.updateRecipient(
+ {
+ id: ps.id,
+ isActive: ps.isActive,
+ name: ps.name,
+ method: ps.method,
+ userId: userId ?? null,
+ systemWebhookId: systemWebhookId ?? null,
+ },
+ me,
+ );
+
+ return this.abuseReportNotificationRecipientEntityService.pack(result);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
index 62358457ff..4e3d731aca 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
@@ -40,7 +40,7 @@ export const paramDef = {
startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' },
},
- required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'],
+ required: ['id'],
} as const;
@Injectable()
@@ -63,8 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ratio: ps.ratio,
memo: ps.memo,
imageUrl: ps.imageUrl,
- expiresAt: new Date(ps.expiresAt),
- startsAt: new Date(ps.startsAt),
+ expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
+ startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek,
});
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
index 87eaad31a3..7596bf44e3 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
@@ -69,6 +69,7 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
+ status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' },
},
required: [],
} as const;
@@ -87,7 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
- query.andWhere('announcement.isActive = true');
+
+ if (ps.status === 'archived') {
+ query.andWhere('announcement.isActive = false');
+ } else if (ps.status === 'active') {
+ query.andWhere('announcement.isActive = true');
+ }
+
if (ps.userId) {
query.andWhere('announcement.userId = :userId', { userId: ps.userId });
} else {
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index 459d8880fa..a7136d8c8c 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -61,7 +61,7 @@ export const meta = {
name: {
type: 'string',
optional: false, nullable: false,
- example: 'lenna.jpg',
+ example: '192.jpg',
},
type: {
type: 'string',
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index eee02a7123..2e7f73da73 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -128,6 +128,16 @@ export const meta = {
nullable: false,
},
},
+ mediaSilencedHosts: {
+ type: 'array',
+ optional: false,
+ nullable: false,
+ items: {
+ type: 'string',
+ optional: false,
+ nullable: false,
+ },
+ },
pinnedUsers: {
type: 'array',
optional: false, nullable: false,
@@ -552,6 +562,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
+ mediaSilencedHosts: instance.mediaSilencedHosts,
sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords,
preservedUsernames: instance.preservedUsernames,
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
index 9694b3fa40..d7f9e4eaa3 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
+import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@@ -53,7 +53,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
super(meta, paramDef, async (ps, me) => {
const deliverJobCounts = await this.deliverQueue.getJobCounts();
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 8b0456068b..9b79100fcf 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -5,12 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, AbuseUserReportsRepository } from '@/models/_.js';
-import { InstanceActorService } from '@/core/InstanceActorService.js';
-import { QueueService } from '@/core/QueueService.js';
-import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
export const meta = {
tags: ['admin'],
@@ -18,6 +16,16 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: 'ac3794dd-2ce4-d878-e546-73c60c06b398',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
} as const;
export const paramDef = {
@@ -29,47 +37,20 @@ export const paramDef = {
required: ['reportId'],
} as const;
-// TODO: ロジックをサービスに切り出す
-
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
-
- private queueService: QueueService,
- private instanceActorService: InstanceActorService,
- private apRendererService: ApRendererService,
- private moderationLogService: ModerationLogService,
+ private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
-
- if (report == null) {
- throw new Error('report not found');
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
}
- if (ps.forward && report.targetUserHost != null) {
- const actor = await this.instanceActorService.getInstanceActor();
- const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
-
- this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false);
- }
-
- await this.abuseUserReportsRepository.update(report.id, {
- resolved: true,
- assigneeId: me.id,
- forwarded: ps.forward && report.targetUserHost != null,
- });
-
- this.moderationLogService.log(me, 'resolveAbuseReport', {
- reportId: report.id,
- report: report,
- forwarded: ps.forward && report.targetUserHost != null,
- });
+ await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
index 5242e0be2f..465ad7aaaf 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RolesRepository } from '@/models/_.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js';
@@ -50,19 +49,6 @@ export const paramDef = {
},
required: [
'roleId',
- 'name',
- 'description',
- 'color',
- 'iconUrl',
- 'target',
- 'condFormula',
- 'isPublic',
- 'isModerator',
- 'isAdministrator',
- 'asBadge',
- 'canEditMembersByModerator',
- 'displayOrder',
- 'policies',
],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts
new file mode 100644
index 0000000000..28071e7a33
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts
@@ -0,0 +1,85 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+ tags: ['admin', 'system-webhook'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'write:admin:system-webhook',
+
+ res: {
+ type: 'object',
+ ref: 'SystemWebhook',
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ isActive: {
+ type: 'boolean',
+ },
+ name: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 255,
+ },
+ on: {
+ type: 'array',
+ items: {
+ type: 'string',
+ enum: systemWebhookEventTypes,
+ },
+ },
+ url: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 1024,
+ },
+ secret: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 1024,
+ },
+ },
+ required: [
+ 'isActive',
+ 'name',
+ 'on',
+ 'url',
+ 'secret',
+ ],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private systemWebhookService: SystemWebhookService,
+ private systemWebhookEntityService: SystemWebhookEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const result = await this.systemWebhookService.createSystemWebhook(
+ {
+ isActive: ps.isActive,
+ name: ps.name,
+ on: ps.on,
+ url: ps.url,
+ secret: ps.secret,
+ },
+ me,
+ );
+
+ return this.systemWebhookEntityService.pack(result);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts
new file mode 100644
index 0000000000..9cdfc7e70f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+ tags: ['admin', 'system-webhook'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'write:admin:system-webhook',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ },
+ required: [
+ 'id',
+ ],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private systemWebhookService: SystemWebhookService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.systemWebhookService.deleteSystemWebhook(
+ ps.id,
+ me,
+ );
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts
new file mode 100644
index 0000000000..7a440a774e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts
@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+ tags: ['admin', 'system-webhook'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'write:admin:system-webhook',
+
+ res: {
+ type: 'array',
+ items: {
+ type: 'object',
+ ref: 'SystemWebhook',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ isActive: {
+ type: 'boolean',
+ },
+ on: {
+ type: 'array',
+ items: {
+ type: 'string',
+ enum: systemWebhookEventTypes,
+ },
+ },
+ },
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private systemWebhookService: SystemWebhookService,
+ private systemWebhookEntityService: SystemWebhookEntityService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const webhooks = await this.systemWebhookService.fetchSystemWebhooks({
+ isActive: ps.isActive,
+ on: ps.on,
+ });
+ return this.systemWebhookEntityService.packMany(webhooks);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts
new file mode 100644
index 0000000000..75862c96a7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { ApiError } from '@/server/api/error.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+ tags: ['admin', 'system-webhook'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'write:admin:system-webhook',
+
+ res: {
+ type: 'object',
+ ref: 'SystemWebhook',
+ },
+
+ errors: {
+ noSuchSystemWebhook: {
+ message: 'No such SystemWebhook.',
+ code: 'NO_SUCH_SYSTEM_WEBHOOK',
+ id: '38dd1ffe-04b4-6ff5-d8ba-4e6a6ae22c9d',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ },
+ required: ['id'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private systemWebhookService: SystemWebhookService,
+ private systemWebhookEntityService: SystemWebhookEntityService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [ps.id] });
+ if (webhooks.length === 0) {
+ throw new ApiError(meta.errors.noSuchSystemWebhook);
+ }
+
+ return this.systemWebhookEntityService.pack(webhooks[0]);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts
new file mode 100644
index 0000000000..8d68bb8f87
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts
@@ -0,0 +1,91 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+
+export const meta = {
+ tags: ['admin', 'system-webhook'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'write:admin:system-webhook',
+
+ res: {
+ type: 'object',
+ ref: 'SystemWebhook',
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ isActive: {
+ type: 'boolean',
+ },
+ name: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 255,
+ },
+ on: {
+ type: 'array',
+ items: {
+ type: 'string',
+ enum: systemWebhookEventTypes,
+ },
+ },
+ url: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 1024,
+ },
+ secret: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 1024,
+ },
+ },
+ required: [
+ 'id',
+ 'isActive',
+ 'name',
+ 'on',
+ 'url',
+ 'secret',
+ ],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private systemWebhookService: SystemWebhookService,
+ private systemWebhookEntityService: SystemWebhookEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const result = await this.systemWebhookService.updateSystemWebhook(
+ {
+ id: ps.id,
+ isActive: ps.isActive,
+ name: ps.name,
+ on: ps.on,
+ url: ps.url,
+ secret: ps.secret,
+ },
+ me,
+ );
+
+ return this.systemWebhookEntityService.pack(result);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 4e28ee6877..5efdc9d8c4 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -150,6 +150,13 @@ export const paramDef = {
type: 'string',
},
},
+ mediaSilencedHosts: {
+ type: 'array',
+ nullable: true,
+ items: {
+ type: 'string',
+ },
+ },
summalyProxy: {
type: 'string', nullable: true,
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
@@ -203,6 +210,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
});
}
+ if (Array.isArray(ps.mediaSilencedHosts)) {
+ let lastValue = '';
+ set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => {
+ const lv = lastValue;
+ lastValue = h;
+ return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
+ });
+ }
if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor;
}
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index ec08198514..577b9e1b1f 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id,
});
- if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
+ if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas);
}
diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts
index 3b44ba81b3..603a3ccf3d 100644
--- a/packages/backend/src/server/api/endpoints/clips/update.ts
+++ b/packages/backend/src/server/api/endpoints/clips/update.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ClipService } from '@/core/ClipService.js';
@@ -41,7 +41,7 @@ export const paramDef = {
isPublic: { type: 'boolean' },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
},
- required: ['clipId', 'name'],
+ required: ['clipId'],
} as const;
@Injectable()
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
index 52b8b335b5..62b04e1df3 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -95,15 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check if the circular reference will occur
const checkCircle = async (folderId: string): Promise<boolean> => {
- // Fetch folder
- const folder2 = await this.driveFoldersRepository.findOneBy({
+ const folder2 = await this.driveFoldersRepository.findOneByOrFail({
id: folderId,
});
- if (folder2!.id === folder!.id) {
+ if (folder2.id === folder.id) {
return true;
- } else if (folder2!.parentId) {
- return await checkCircle(folder2!.parentId);
+ } else if (folder2.parentId) {
+ return await checkCircle(folder2.parentId);
} else {
return false;
}
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 4ef4315fb3..36f4bf5aa6 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -117,9 +117,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.suspended === 'boolean') {
if (ps.suspended) {
- query.andWhere('instance.isSuspended = TRUE');
+ query.andWhere('instance.suspensionState != \'none\'');
} else {
- query.andWhere('instance.isSuspended = FALSE');
+ query.andWhere('instance.suspensionState = \'none\'');
}
}
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
index 46f8998810..504a9c789e 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
@@ -12,7 +12,6 @@ import type { MiDriveFile } from '@/models/DriveFile.js';
import { IdService } from '@/core/IdService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js';
-import { isNotNull } from '@/misc/is-not-null.js';
export const meta = {
tags: ['gallery'],
@@ -70,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: fileId,
userId: me.id,
}),
- ))).filter(isNotNull);
+ ))).filter(x => x != null);
if (files.length === 0) {
throw new Error();
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
index 8bd83ff5ba..5243ee9603 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
@@ -10,7 +10,6 @@ import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js
import type { MiDriveFile } from '@/models/DriveFile.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js';
-import { isNotNull } from '@/misc/is-not-null.js';
export const meta = {
tags: ['gallery'],
@@ -48,7 +47,7 @@ export const paramDef = {
} },
isSensitive: { type: 'boolean', default: false },
},
- required: ['postId', 'title', 'fileIds'],
+ required: ['postId'],
} as const;
@Injectable()
@@ -63,15 +62,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private galleryPostEntityService: GalleryPostEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const files = (await Promise.all(ps.fileIds.map(fileId =>
- this.driveFilesRepository.findOneBy({
- id: fileId,
- userId: me.id,
- }),
- ))).filter(isNotNull);
+ let files: Array<MiDriveFile> | undefined;
- if (files.length === 0) {
- throw new Error();
+ if (ps.fileIds) {
+ files = (await Promise.all(ps.fileIds.map(fileId =>
+ this.driveFilesRepository.findOneBy({
+ id: fileId,
+ userId: me.id,
+ }),
+ ))).filter(x => x != null);
+
+ if (files.length === 0) {
+ throw new Error();
+ }
}
await this.galleryPostsRepository.update({
@@ -82,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
title: ps.title,
description: ps.description,
isSensitive: ps.isSensitive,
- fileIds: files.map(file => file.id),
+ fileIds: files ? files.map(file => file.id) : undefined,
});
const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId });
diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
index b4661a93e2..bc46163e3d 100644
--- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
@@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));
const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
- if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
+ if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas);
}
this.queueService.createImportAntennasJob(me, antennas);
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index a8e702f328..a1e2fa5e4c 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js';
-import { RoleService } from '@/core/RoleService.js';
+import { RolePolicies, RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
@@ -256,8 +256,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profileUpdates = {} as Partial<MiUserProfile>;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+ let policies: RolePolicies | null = null;
- if (ps.name !== undefined) updates.name = ps.name;
+ if (ps.name !== undefined) {
+ if (ps.name === null) {
+ updates.name = null;
+ } else {
+ const trimmedName = ps.name.trim();
+ updates.name = trimmedName === '' ? null : trimmedName;
+ }
+ }
if (ps.description !== undefined) profileUpdates.description = ps.description;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location;
@@ -289,14 +297,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.mutedWords !== undefined) {
- checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit);
validateMuteWordRegex(ps.mutedWords);
profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
if (ps.hardMutedWords !== undefined) {
- checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit);
validateMuteWordRegex(ps.hardMutedWords);
profileUpdates.hardMutedWords = ps.hardMutedWords;
}
@@ -315,13 +325,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === 'boolean') {
- if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
}
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
if (ps.avatarId) {
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
+
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
@@ -337,6 +351,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.bannerId) {
+ policies ??= await this.roleService.getUserPolicies(user.id);
+ if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
+
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
@@ -352,14 +369,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.avatarDecorations) {
+ policies ??= await this.roleService.getUserPolicies(user.id);
const decorations = await this.avatarDecorationService.getAll(true);
- const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
+ const myRoles = await this.roleService.getUserRoles(user.id);
const allRoles = await this.roleService.getRoles();
const decorationIds = decorations
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
.map(d => d.id);
- if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
+ if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
id: d.id,
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
index c692380288..9eb7f5b3a0 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentWebhooksCount = await this.webhooksRepository.countBy({
userId: me.id,
});
- if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
+ if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
throw new ApiError(meta.errors.tooManyWebhooks);
}
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
index 6e380d76f8..07a25bd82a 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
@@ -34,13 +34,13 @@ export const paramDef = {
webhookId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 },
url: { type: 'string', minLength: 1, maxLength: 1024 },
- secret: { type: 'string', maxLength: 1024, default: '' },
+ secret: { type: 'string', nullable: true, maxLength: 1024 },
on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes,
} },
active: { type: 'boolean' },
},
- required: ['webhookId', 'name', 'url', 'on', 'active'],
+ required: ['webhookId'],
} as const;
// TODO: ロジックをサービスに切り出す
@@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.webhooksRepository.update(webhook.id, {
name: ps.name,
url: ps.url,
- secret: ps.secret,
+ secret: ps.secret === null ? '' : ps.secret,
on: ps.on,
active: ps.active,
});
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index 5acc9706d3..2a2c659942 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -139,9 +139,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
timelineConfig = [
`homeTimeline:${me.id}`,
'localTimeline',
+ `localTimelineWithReplyTo:${me.id}`,
];
}
+ const [
+ followings,
+ ] = await Promise.all([
+ this.cacheService.userFollowingsCache.fetch(me.id),
+ ]);
+
const redisTimeline = await this.fanoutTimelineEndpointService.timeline({
untilId,
sinceId,
@@ -152,6 +159,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
+ noteFilter: note => {
+ if (note.reply && note.reply.visibility === 'followers') {
+ if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
+ }
+
+ return true;
+ },
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
untilId,
sinceId,
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
index b9899608bf..0f0dcca605 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
@@ -36,6 +36,12 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
},
+
+ cannotReactToRenote: {
+ message: 'You cannot react to Renote.',
+ code: 'CANNOT_REACT_TO_RENOTE',
+ id: 'eaccdc08-ddef-43fe-908f-d108faad57f5',
+ },
},
} as const;
@@ -62,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.reactionService.create(me, note, ps.reaction).catch(err => {
if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
+ if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote);
throw err;
});
return;
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 8b87908bd3..c9b43b5359 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -114,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludePureRenotes: !ps.withRenotes,
noteFilter: note => {
if (note.reply && note.reply.visibility === 'followers') {
- if (!Object.hasOwn(followings, note.reply.userId)) return false;
+ if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
}
return true;
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
index b8e5e70a25..f11bbbcb1a 100644
--- a/packages/backend/src/server/api/endpoints/pages/update.ts
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -70,7 +70,7 @@ export const paramDef = {
alignCenter: { type: 'boolean' },
hideTitleWhenPinned: { type: 'boolean' },
},
- required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
+ required: ['pageId'],
} as const;
@Injectable()
@@ -91,9 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.accessDenied);
}
- let eyeCatchingImage = null;
if (ps.eyeCatchingImageId != null) {
- eyeCatchingImage = await this.driveFilesRepository.findOneBy({
+ const eyeCatchingImage = await this.driveFilesRepository.findOneBy({
id: ps.eyeCatchingImageId,
userId: me.id,
});
@@ -116,23 +115,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.pagesRepository.update(page.id, {
updatedAt: new Date(),
title: ps.title,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- name: ps.name === undefined ? page.name : ps.name,
+ name: ps.name,
summary: ps.summary === undefined ? page.summary : ps.summary,
content: ps.content,
variables: ps.variables,
script: ps.script,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- font: ps.font === undefined ? page.font : ps.font,
- eyeCatchingImageId: ps.eyeCatchingImageId === null
- ? null
- : ps.eyeCatchingImageId === undefined
- ? page.eyeCatchingImageId
- : eyeCatchingImage!.id,
+ alignCenter: ps.alignCenter,
+ hideTitleWhenPinned: ps.hideTitleWhenPinned,
+ font: ps.font,
+ eyeCatchingImageId: ps.eyeCatchingImageId,
});
});
}
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 784766bcb5..15832ef7f8 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -12,7 +12,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
-import { isNotNull } from '@/misc/is-not-null.js';
export const meta = {
tags: ['users'],
@@ -53,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
host: acct.host ?? IsNull(),
})));
- return await this.userEntityService.packMany(users.filter(isNotNull), me, { schema: 'UserDetailed' });
+ return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: 'UserDetailed' });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
index 39bf0cc428..84a1f010d4 100644
--- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts
+++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
@@ -6,12 +6,11 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { IdService } from '@/core/IdService.js';
-import type { RenoteMutingsRepository } from '@/models/_.js';
-import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
+import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
+import type { RenoteMutingsRepository } from '@/models/_.js';
export const meta = {
tags: ['account'],
@@ -62,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private renoteMutingsRepository: RenoteMutingsRepository,
private getterService: GetterService,
- private idService: IdService,
+ private userRenoteMutingService: UserRenoteMutingService,
) {
super(meta, paramDef, async (ps, me) => {
const muter = me;
@@ -79,21 +78,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check if already muting
- const exist = await this.renoteMutingsRepository.findOneBy({
- muterId: muter.id,
- muteeId: mutee.id,
+ const exist = await this.renoteMutingsRepository.exists({
+ where: {
+ muterId: muter.id,
+ muteeId: mutee.id,
+ },
});
- if (exist != null) {
+ if (exist === true) {
throw new ApiError(meta.errors.alreadyMuting);
}
// Create mute
- await this.renoteMutingsRepository.insert({
- id: this.idService.gen(),
- muterId: muter.id,
- muteeId: mutee.id,
- } as MiRenoteMuting);
+ await this.userRenoteMutingService.mute(muter, mutee);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts
index 6e037cc07e..1a584b8404 100644
--- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts
+++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts
@@ -5,10 +5,11 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { RenoteMutingsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
+import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
+import type { RenoteMutingsRepository } from '@/models/_.js';
export const meta = {
tags: ['account'],
@@ -53,6 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private renoteMutingsRepository: RenoteMutingsRepository,
private getterService: GetterService,
+ private userRenoteMutingService: UserRenoteMutingService,
) {
super(meta, paramDef, async (ps, me) => {
const muter = me;
@@ -79,9 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Delete mute
- await this.renoteMutingsRepository.delete({
- id: exist.id,
- });
+ await this.userRenoteMutingService.unmute([exist]);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
index 8504da0209..7e44d501ab 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
@@ -100,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
- if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
+ if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists);
}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts
index 9378bde5cb..7daf05ba4e 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
- if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
+ if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists);
}
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
index aca883a052..7805ae3288 100644
--- a/packages/backend/src/server/api/endpoints/users/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { RoleService } from '@/core/RoleService.js';
+import { isUserRelated } from '@/misc/is-user-related.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
+ const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>();
const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
if (!iAmModerator) {
const user = await this.cacheService.findUserById(ps.userId);
@@ -85,8 +87,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
throw new ApiError(meta.errors.reactionsNotPublic);
}
+
+ // early return if me is blocked by requesting user
+ if (userIdsWhoBlockingMe.has(ps.userId)) {
+ return [];
+ }
}
+ const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>();
+
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('reaction.userId = :userId', { userId: ps.userId })
@@ -94,9 +103,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
- const reactions = await query
+ const reactions = (await query
.limit(ps.limit)
- .getMany();
+ .getMany()).filter(reaction => {
+ if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user
+ if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false;
+ if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false;
+
+ return true;
+ });
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
});
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
index 48e14b68cc..5ff6de37d2 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -3,17 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import sanitizeHtml from 'sanitize-html';
-import { Inject, Injectable } from '@nestjs/common';
-import type { AbuseUserReportsRepository } from '@/models/_.js';
-import { IdService } from '@/core/IdService.js';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { MetaService } from '@/core/MetaService.js';
-import { EmailService } from '@/core/EmailService.js';
-import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -57,60 +51,32 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.abuseUserReportsRepository)
- private abuseUserReportsRepository: AbuseUserReportsRepository,
-
- private idService: IdService,
- private metaService: MetaService,
- private emailService: EmailService,
private getterService: GetterService,
private roleService: RoleService,
- private globalEventService: GlobalEventService,
+ private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
// Lookup user
- const user = await this.getterService.getUser(ps.userId).catch(err => {
+ const targetUser = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
- if (user.id === me.id) {
+ if (targetUser.id === me.id) {
throw new ApiError(meta.errors.cannotReportYourself);
}
- if (await this.roleService.isAdministrator(user)) {
+ if (await this.roleService.isAdministrator(targetUser)) {
throw new ApiError(meta.errors.cannotReportAdmin);
}
- const report = await this.abuseUserReportsRepository.insertOne({
- id: this.idService.gen(),
- targetUserId: user.id,
- targetUserHost: user.host,
+ await this.abuseReportService.report([{
+ targetUserId: targetUser.id,
+ targetUserHost: targetUser.host,
reporterId: me.id,
reporterHost: null,
comment: ps.comment,
- });
-
- // Publish event to moderators
- setImmediate(async () => {
- const moderators = await this.roleService.getModerators();
-
- for (const moderator of moderators) {
- this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', {
- id: report.id,
- targetUserId: report.targetUserId,
- reporterId: report.reporterId,
- comment: report.comment,
- });
- }
-
- const meta = await this.metaService.fetch();
- if (meta.email) {
- this.emailService.sendEmail(meta.email, 'New abuse report',
- sanitizeHtml(ps.comment),
- sanitizeHtml(ps.comment));
- }
- });
+ }]);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 7b3bdab327..8ff952dcb5 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -3,15 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Brackets } from 'typeorm';
-import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository, FollowingsRepository } from '@/models/_.js';
-import type { Config } from '@/config.js';
-import type { MiUser } from '@/models/User.js';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { DI } from '@/di-symbols.js';
-import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import { UserSearchService } from '@/core/UserSearchService.js';
export const meta = {
tags: ['users'],
@@ -49,89 +43,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.config)
- private config: Config,
-
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- @Inject(DI.followingsRepository)
- private followingsRepository: FollowingsRepository,
-
- private userEntityService: UserEntityService,
+ private userSearchService: UserSearchService,
) {
- super(meta, paramDef, async (ps, me) => {
- const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => {
- if (ps.username) {
- query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
- }
-
- if (ps.host) {
- if (ps.host === this.config.hostname || ps.host === '.') {
- query.andWhere('user.host IS NULL');
- } else {
- query.andWhere('user.host LIKE :host', {
- host: sqlLikeEscape(ps.host.toLowerCase()) + '%',
- });
- }
- }
-
- return query;
- };
-
- const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
-
- let users: MiUser[] = [];
-
- if (me) {
- const followingQuery = this.followingsRepository.createQueryBuilder('following')
- .select('following.followeeId')
- .where('following.followerId = :followerId', { followerId: me.id });
-
- const query = setUsernameAndHostQuery()
- .andWhere(`user.id IN (${ followingQuery.getQuery() })`)
- .andWhere('user.id != :meId', { meId: me.id })
- .andWhere('user.isSuspended = FALSE')
- .andWhere(new Brackets(qb => {
- qb
- .where('user.updatedAt IS NULL')
- .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
- }));
-
- query.setParameters(followingQuery.getParameters());
-
- users = await query
- .orderBy('user.usernameLower', 'ASC')
- .limit(ps.limit)
- .getMany();
-
- if (users.length < ps.limit) {
- const otherQuery = setUsernameAndHostQuery()
- .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`)
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.updatedAt IS NOT NULL');
-
- otherQuery.setParameters(followingQuery.getParameters());
-
- const otherUsers = await otherQuery
- .orderBy('user.updatedAt', 'DESC')
- .limit(ps.limit - users.length)
- .getMany();
-
- users = users.concat(otherUsers);
- }
- } else {
- const query = setUsernameAndHostQuery()
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.updatedAt IS NOT NULL');
-
- users = await query
- .orderBy('user.updatedAt', 'DESC')
- .limit(ps.limit - users.length)
- .getMany();
- }
-
- return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
+ super(meta, paramDef, (ps, me) => {
+ return this.userSearchService.search({
+ username: ps.username,
+ host: ps.host,
+ }, {
+ limit: ps.limit,
+ detail: ps.detail,
+ }, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
index df9d9f6312..0b0136066d 100644
--- a/packages/backend/src/server/api/endpoints/users/search.ts
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -57,88 +57,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
ps.query = ps.query.trim();
- const isUsername = ps.query.startsWith('@');
+ const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
let users: MiUser[] = [];
- if (isUsername) {
- const usernameQuery = this.usersRepository.createQueryBuilder('user')
- .where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' })
- .andWhere(new Brackets(qb => {
- qb
- .where('user.updatedAt IS NULL')
- .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
- }))
- .andWhere('user.isSuspended = FALSE');
+ const nameQuery = this.usersRepository.createQueryBuilder('user')
+ .where(new Brackets(qb => {
+ qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
+
+ if (isUsername) {
+ qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
+ } else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
+ qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
+ }
+ }))
+ .andWhere(new Brackets(qb => {
+ qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }))
+ .andWhere('user.isSuspended = FALSE');
+
+ if (ps.origin === 'local') {
+ nameQuery.andWhere('user.host IS NULL');
+ } else if (ps.origin === 'remote') {
+ nameQuery.andWhere('user.host IS NOT NULL');
+ }
+
+ users = await nameQuery
+ .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+ .limit(ps.limit)
+ .offset(ps.offset)
+ .getMany();
+
+ if (users.length < ps.limit) {
+ const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
+ .select('prof.userId')
+ .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
- usernameQuery.andWhere('user.host IS NULL');
+ profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
- usernameQuery.andWhere('user.host IS NOT NULL');
+ profQuery.andWhere('prof.userHost IS NOT NULL');
}
- users = await usernameQuery
- .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
- .limit(ps.limit)
- .offset(ps.offset)
- .getMany();
- } else {
- const nameQuery = this.usersRepository.createQueryBuilder('user')
- .where(new Brackets(qb => {
- qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
-
- // Also search username if it qualifies as username
- if (this.userEntityService.validateLocalUsername(ps.query)) {
- qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
- }
- }))
+ const query = this.usersRepository.createQueryBuilder('user')
+ .where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
- .andWhere('user.isSuspended = FALSE');
-
- if (ps.origin === 'local') {
- nameQuery.andWhere('user.host IS NULL');
- } else if (ps.origin === 'remote') {
- nameQuery.andWhere('user.host IS NOT NULL');
- }
+ .andWhere('user.isSuspended = FALSE')
+ .setParameters(profQuery.getParameters());
- users = await nameQuery
+ users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
- .getMany();
-
- if (users.length < ps.limit) {
- const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
- .select('prof.userId')
- .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
-
- if (ps.origin === 'local') {
- profQuery.andWhere('prof.userHost IS NULL');
- } else if (ps.origin === 'remote') {
- profQuery.andWhere('prof.userHost IS NOT NULL');
- }
-
- const query = this.usersRepository.createQueryBuilder('user')
- .where(`user.id IN (${ profQuery.getQuery() })`)
- .andWhere(new Brackets(qb => {
- qb
- .where('user.updatedAt IS NULL')
- .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
- }))
- .andWhere('user.isSuspended = FALSE')
- .setParameters(profQuery.getParameters());
-
- users = users.concat(await query
- .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
- .limit(ps.limit)
- .offset(ps.offset)
- .getMany(),
- );
- }
+ .getMany(),
+ );
}
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts
index 5210e4d2bc..f124aa9f39 100644
--- a/packages/backend/src/server/api/openapi/OpenApiServerService.ts
+++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts
@@ -25,7 +25,7 @@ export class OpenApiServerService {
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/api-doc', async (_request, reply) => {
reply.header('Cache-Control', 'public, max-age=86400');
- return await reply.sendFile('/redoc.html', staticAssets);
+ return await reply.sendFile('/api-doc.html', staticAssets);
});
fastify.get('/api.json', (_request, reply) => {
reply.header('Cache-Control', 'public, max-age=600');
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
index 2a14270a24..efa47a6986 100644
--- a/packages/backend/src/server/api/openapi/gen-spec.ts
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -15,7 +15,6 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
info: {
version: config.version,
title: 'Misskey API',
- 'x-logo': { url: '/static-assets/api-doc.png' },
},
externalDocs: {
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index 41c0feccc7..96082827f8 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -14,6 +14,7 @@ import { CacheService } from '@/core/CacheService.js';
import { MiFollowing, MiUserProfile } from '@/models/_.js';
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
+import type { JsonObject } from '@/misc/json-value.js';
import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
@@ -28,7 +29,7 @@ export default class Connection {
private wsConnection: WebSocket.WebSocket;
public subscriber: StreamEventEmitter;
private channels: Channel[] = [];
- private subscribingNotes: any = {};
+ private subscribingNotes: Partial<Record<string, number>> = {};
private cachedNotes: Packed<'Note'>[] = [];
public userProfile: MiUserProfile | null = null;
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
@@ -101,7 +102,7 @@ export default class Connection {
*/
@bindThis
private async onWsConnectionMessage(data: WebSocket.RawData) {
- let obj: Record<string, any>;
+ let obj: JsonObject;
try {
obj = JSON.parse(data.toString());
@@ -111,6 +112,8 @@ export default class Connection {
const { type, body } = obj;
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
+
switch (type) {
case 'readNotification': this.onReadNotification(body); break;
case 'subNote': this.onSubscribeNote(body); break;
@@ -151,7 +154,7 @@ export default class Connection {
}
@bindThis
- private readNote(body: any) {
+ private readNote(body: JsonObject) {
const id = body.id;
const note = this.cachedNotes.find(n => n.id === id);
@@ -163,7 +166,7 @@ export default class Connection {
}
@bindThis
- private onReadNotification(payload: any) {
+ private onReadNotification(payload: JsonObject) {
this.notificationService.readAllNotification(this.user!.id);
}
@@ -171,16 +174,14 @@ export default class Connection {
* 投稿購読要求時
*/
@bindThis
- private onSubscribeNote(payload: any) {
- if (!payload.id) return;
-
- if (this.subscribingNotes[payload.id] == null) {
- this.subscribingNotes[payload.id] = 0;
- }
+ private onSubscribeNote(payload: JsonObject) {
+ if (!payload.id || typeof payload.id !== 'string') return;
- this.subscribingNotes[payload.id]++;
+ const current = this.subscribingNotes[payload.id] ?? 0;
+ const updated = current + 1;
+ this.subscribingNotes[payload.id] = updated;
- if (this.subscribingNotes[payload.id] === 1) {
+ if (updated === 1) {
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
}
}
@@ -189,11 +190,14 @@ export default class Connection {
* 投稿購読解除要求時
*/
@bindThis
- private onUnsubscribeNote(payload: any) {
- if (!payload.id) return;
+ private onUnsubscribeNote(payload: JsonObject) {
+ if (!payload.id || typeof payload.id !== 'string') return;
- this.subscribingNotes[payload.id]--;
- if (this.subscribingNotes[payload.id] <= 0) {
+ const current = this.subscribingNotes[payload.id];
+ if (current == null) return;
+ const updated = current - 1;
+ this.subscribingNotes[payload.id] = updated;
+ if (updated <= 0) {
delete this.subscribingNotes[payload.id];
this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
}
@@ -212,17 +216,22 @@ export default class Connection {
* チャンネル接続要求時
*/
@bindThis
- private onChannelConnectRequested(payload: any) {
+ private onChannelConnectRequested(payload: JsonObject) {
const { channel, id, params, pong } = payload;
- this.connectChannel(id, params, channel, pong);
+ if (typeof id !== 'string') return;
+ if (typeof channel !== 'string') return;
+ if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return;
+ if (typeof params !== 'undefined' && (typeof params !== 'object' || params === null || Array.isArray(params))) return;
+ this.connectChannel(id, params, channel, pong ?? undefined);
}
/**
* チャンネル切断要求時
*/
@bindThis
- private onChannelDisconnectRequested(payload: any) {
+ private onChannelDisconnectRequested(payload: JsonObject) {
const { id } = payload;
+ if (typeof id !== 'string') return;
this.disconnectChannel(id);
}
@@ -230,7 +239,7 @@ export default class Connection {
* クライアントにメッセージ送信
*/
@bindThis
- public sendMessageToWs(type: string, payload: any) {
+ public sendMessageToWs(type: string, payload: JsonObject) {
this.wsConnection.send(JSON.stringify({
type: type,
body: payload,
@@ -241,7 +250,7 @@ export default class Connection {
* チャンネルに接続
*/
@bindThis
- public connectChannel(id: string, params: any, channel: string, pong = false) {
+ public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
const channelService = this.channelsService.getChannelService(channel);
if (channelService.requireCredential && this.user == null) {
@@ -288,7 +297,11 @@ export default class Connection {
* @param data メッセージ
*/
@bindThis
- private onChannelMessageRequested(data: any) {
+ private onChannelMessageRequested(data: JsonObject) {
+ if (typeof data.id !== 'string') return;
+ if (typeof data.type !== 'string') return;
+ if (typeof data.body === 'undefined') return;
+
const channel = this.channels.find(c => c.id === data.id);
if (channel != null && channel.onMessage != null) {
channel.onMessage(data.type, data.body);
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index a267d27fba..84cb552369 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { Packed } from '@/misc/json-schema.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import type Connection from './Connection.js';
/**
@@ -81,10 +82,12 @@ export default abstract class Channel {
this.connection = connection;
}
+ public send(payload: { type: string, body: JsonValue }): void
+ public send(type: string, payload: JsonValue): void
@bindThis
- public send(typeOrPayload: any, payload?: any) {
- const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
- const body = payload === undefined ? typeOrPayload.body : payload;
+ public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
+ const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
+ const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload;
this.connection.sendMessageToWs('channel', {
id: this.id,
@@ -93,11 +96,11 @@ export default abstract class Channel {
});
}
- public abstract init(params: any): void;
+ public abstract init(params: JsonObject): void;
public dispose?(): void;
- public onMessage?(type: string, body: any): void;
+ public onMessage?(type: string, body: JsonValue): void;
}
export type MiChannelService<T extends boolean> = {
diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts
index 92b6d2ac04..355d5dba21 100644
--- a/packages/backend/src/server/api/stream/channels/admin.ts
+++ b/packages/backend/src/server/api/stream/channels/admin.ts
@@ -5,6 +5,7 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class AdminChannel extends Channel {
@@ -14,7 +15,7 @@ class AdminChannel extends Channel {
public static kind = 'read:admin:stream';
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
// Subscribe admin stream
this.subscriber.on(`adminStream:${this.user!.id}`, data => {
this.send(data);
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 4a1d2dd109..53dc7f18b6 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class AntennaChannel extends Channel {
@@ -27,8 +28,9 @@ class AntennaChannel extends Channel {
}
@bindThis
- public async init(params: any) {
- this.antennaId = params.antennaId as string;
+ public async init(params: JsonObject) {
+ if (typeof params.antennaId !== 'string') return;
+ this.antennaId = params.antennaId;
// Subscribe stream
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 140dd3dd9b..7108e0cd6e 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class ChannelChannel extends Channel {
@@ -27,8 +28,9 @@ class ChannelChannel extends Channel {
}
@bindThis
- public async init(params: any) {
- this.channelId = params.channelId as string;
+ public async init(params: JsonObject) {
+ if (typeof params.channelId !== 'string') return;
+ this.channelId = params.channelId;
// Subscribe stream
this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts
index 0d9b486305..03768f3d23 100644
--- a/packages/backend/src/server/api/stream/channels/drive.ts
+++ b/packages/backend/src/server/api/stream/channels/drive.ts
@@ -5,6 +5,7 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class DriveChannel extends Channel {
@@ -14,7 +15,7 @@ class DriveChannel extends Channel {
public static kind = 'read:account';
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
// Subscribe drive stream
this.subscriber.on(`driveStream:${this.user!.id}`, data => {
this.send(data);
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 17116258d8..ed56fe0d40 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class GlobalTimelineChannel extends Channel {
@@ -32,12 +33,12 @@ class GlobalTimelineChannel extends Channel {
}
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.gtlAvailable) return;
- this.withRenotes = params.withRenotes ?? true;
- this.withFiles = params.withFiles ?? false;
+ this.withRenotes = !!(params.withRenotes ?? true);
+ this.withFiles = !!(params.withFiles ?? false);
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
index 57bada5d9c..8105f15cb1 100644
--- a/packages/backend/src/server/api/stream/channels/hashtag.ts
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -9,6 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class HashtagChannel extends Channel {
@@ -28,11 +29,11 @@ class HashtagChannel extends Channel {
}
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
+ if (!Array.isArray(params.q)) return;
+ if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return;
this.q = params.q;
- if (this.q == null) return;
-
// Subscribe stream
this.subscriber.on('notesStream', this.onNote);
}
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index 878a3180cb..66644ed58c 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class HomeTimelineChannel extends Channel {
@@ -29,9 +30,9 @@ class HomeTimelineChannel extends Channel {
}
@bindThis
- public async init(params: any) {
- this.withRenotes = params.withRenotes ?? true;
- this.withFiles = params.withFiles ?? false;
+ public async init(params: JsonObject) {
+ this.withRenotes = !!(params.withRenotes ?? true);
+ this.withFiles = !!(params.withFiles ?? false);
this.subscriber.on('notesStream', this.onNote);
}
@@ -59,7 +60,7 @@ class HomeTimelineChannel extends Channel {
const reply = note.reply;
if (this.following[note.userId]?.withReplies) {
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
- if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
+ if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
} else {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
@@ -72,7 +73,7 @@ class HomeTimelineChannel extends Channel {
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
- if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
+ if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
}
}
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 575d23d53c..75bd13221f 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class HybridTimelineChannel extends Channel {
@@ -34,13 +35,13 @@ class HybridTimelineChannel extends Channel {
}
@bindThis
- public async init(params: any): Promise<void> {
+ public async init(params: JsonObject): Promise<void> {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
- this.withRenotes = params.withRenotes ?? true;
- this.withReplies = params.withReplies ?? false;
- this.withFiles = params.withFiles ?? false;
+ this.withRenotes = !!(params.withRenotes ?? true);
+ this.withReplies = !!(params.withReplies ?? false);
+ this.withFiles = !!(params.withFiles ?? false);
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@@ -75,14 +76,22 @@ class HybridTimelineChannel extends Channel {
const reply = note.reply;
if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) {
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
- if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
+ if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
} else {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
}
}
- if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
+ // 純粋なリノート(引用リノートでないリノート)の場合
+ if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
+ if (!this.withRenotes) return;
+ if (note.renote.reply) {
+ const reply = note.renote.reply;
+ // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
+ if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
+ }
+ }
if (this.user && note.renoteId && !note.text) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 442d08ae51..491029f5de 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class LocalTimelineChannel extends Channel {
@@ -33,13 +34,13 @@ class LocalTimelineChannel extends Channel {
}
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
- this.withRenotes = params.withRenotes ?? true;
- this.withReplies = params.withReplies ?? false;
- this.withFiles = params.withFiles ?? false;
+ this.withRenotes = !!(params.withRenotes ?? true);
+ this.withReplies = !!(params.withReplies ?? false);
+ this.withFiles = !!(params.withFiles ?? false);
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts
index a12976d69d..863d7f4c4e 100644
--- a/packages/backend/src/server/api/stream/channels/main.ts
+++ b/packages/backend/src/server/api/stream/channels/main.ts
@@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class MainChannel extends Channel {
@@ -25,7 +26,7 @@ class MainChannel extends Channel {
}
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
// Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
switch (data.type) {
diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts
index 061aa76904..ff7e740226 100644
--- a/packages/backend/src/server/api/stream/channels/queue-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts
@@ -6,6 +6,7 @@
import Xev from 'xev';
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev();
@@ -22,19 +23,22 @@ class QueueStatsChannel extends Channel {
}
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
ev.addListener('queueStats', this.onStats);
}
@bindThis
- private onStats(stats: any) {
+ private onStats(stats: JsonObject) {
this.send('stats', stats);
}
@bindThis
- public onMessage(type: string, body: any) {
+ public onMessage(type: string, body: JsonValue) {
switch (type) {
case 'requestLog':
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
+ if (typeof body.id !== 'string') return;
+ if (typeof body.length !== 'number') return;
ev.once(`queueStatsLog:${body.id}`, statsLog => {
this.send('statsLog', statsLog);
});
diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts
index f4a3a09367..17823a164a 100644
--- a/packages/backend/src/server/api/stream/channels/reversi-game.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts
@@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { ReversiService } from '@/core/ReversiService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class ReversiGameChannel extends Channel {
@@ -28,25 +29,41 @@ class ReversiGameChannel extends Channel {
}
@bindThis
- public async init(params: any) {
- this.gameId = params.gameId as string;
+ public async init(params: JsonObject) {
+ if (typeof params.gameId !== 'string') return;
+ this.gameId = params.gameId;
this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
}
@bindThis
- public onMessage(type: string, body: any) {
+ public onMessage(type: string, body: JsonValue) {
switch (type) {
- case 'ready': this.ready(body); break;
- case 'updateSettings': this.updateSettings(body.key, body.value); break;
- case 'cancel': this.cancelGame(); break;
- case 'putStone': this.putStone(body.pos, body.id); break;
+ case 'ready':
+ if (typeof body !== 'boolean') return;
+ this.ready(body);
+ break;
+ case 'updateSettings':
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
+ if (typeof body.key !== 'string') return;
+ if (typeof body.value !== 'object' || body.value === null || Array.isArray(body.value)) return;
+ this.updateSettings(body.key, body.value);
+ break;
+ case 'cancel':
+ this.cancelGame();
+ break;
+ case 'putStone':
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
+ if (typeof body.pos !== 'number') return;
+ if (typeof body.id !== 'string') return;
+ this.putStone(body.pos, body.id);
+ break;
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
}
}
@bindThis
- private async updateSettings(key: string, value: any) {
+ private async updateSettings(key: string, value: JsonObject) {
if (this.user == null) return;
this.reversiService.updateSettings(this.gameId!, this.user, key, value);
diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts
index 3998a0fd36..6e88939724 100644
--- a/packages/backend/src/server/api/stream/channels/reversi.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi.ts
@@ -5,6 +5,7 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class ReversiChannel extends Channel {
@@ -21,7 +22,7 @@ class ReversiChannel extends Channel {
}
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
this.subscriber.on(`reversiStream:${this.user!.id}`, this.send);
}
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index 6a4ad22460..fcfa26c38b 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -8,6 +8,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class RoleTimelineChannel extends Channel {
@@ -28,8 +29,9 @@ class RoleTimelineChannel extends Channel {
}
@bindThis
- public async init(params: any) {
- this.roleId = params.roleId as string;
+ public async init(params: JsonObject) {
+ if (typeof params.roleId !== 'string') return;
+ this.roleId = params.roleId;
this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
}
diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts
index eb4d8c9992..6258afba35 100644
--- a/packages/backend/src/server/api/stream/channels/server-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/server-stats.ts
@@ -6,6 +6,7 @@
import Xev from 'xev';
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
+import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev();
@@ -22,19 +23,20 @@ class ServerStatsChannel extends Channel {
}
@bindThis
- public async init(params: any) {
+ public async init(params: JsonObject) {
ev.addListener('serverStats', this.onStats);
}
@bindThis
- private onStats(stats: any) {
+ private onStats(stats: JsonObject) {
this.send('stats', stats);
}
@bindThis
- public onMessage(type: string, body: any) {
+ public onMessage(type: string, body: JsonValue) {
switch (type) {
case 'requestLog':
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
ev.once(`serverStatsLog:${body.id}`, statsLog => {
this.send('statsLog', statsLog);
});
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index 14b30a157c..4f38351e94 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
class UserListChannel extends Channel {
@@ -36,10 +37,11 @@ class UserListChannel extends Channel {
}
@bindThis
- public async init(params: any) {
- this.listId = params.listId as string;
- this.withFiles = params.withFiles ?? false;
- this.withRenotes = params.withRenotes ?? true;
+ public async init(params: JsonObject) {
+ if (typeof params.listId !== 'string') return;
+ this.listId = params.listId;
+ this.withFiles = !!(params.withFiles ?? false);
+ this.withRenotes = !!(params.withRenotes ?? true);
// Check existence and owner
const listExist = await this.userListsRepository.exists({
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index ab03489c0d..f55790b636 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -25,7 +25,16 @@ import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import { MetaService } from '@/core/MetaService.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
+import type {
+ DbQueue,
+ DeliverQueue,
+ EndedPollNotificationQueue,
+ InboxQueue,
+ ObjectStorageQueue,
+ SystemQueue,
+ UserWebhookDeliverQueue,
+ SystemWebhookDeliverQueue,
+} from '@/core/QueueModule.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
@@ -111,7 +120,8 @@ export class ClientServerService {
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
- @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
+ @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
+ @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
//this.createServer = this.createServer.bind(this);
}
@@ -239,7 +249,8 @@ export class ClientServerService {
this.inboxQueue,
this.dbQueue,
this.objectStorageQueue,
- this.webhookDeliverQueue,
+ this.userWebhookDeliverQueue,
+ this.systemWebhookDeliverQueue,
].map(q => new BullMQAdapter(q)),
serverAdapter,
});
diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts
index 10e3ed2682..9d810ddc84 100644
--- a/packages/backend/src/server/web/FeedService.ts
+++ b/packages/backend/src/server/web/FeedService.ts
@@ -14,6 +14,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { MfmService } from "@/core/MfmService.js";
+import { parse as mfmParse } from 'mfm-js';
@Injectable()
export class FeedService {
@@ -33,6 +35,7 @@ export class FeedService {
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private idService: IdService,
+ private mfmService: MfmService,
) {
}
@@ -76,13 +79,14 @@ export class FeedService {
id: In(note.fileIds),
}) : [];
const file = files.find(file => file.type.startsWith('image/'));
+ const text = note.text;
feed.addItem({
title: `New note by ${author.name}`,
link: `${this.config.url}/notes/${note.id}`,
date: this.idService.parse(note.id).date,
description: note.cw ?? undefined,
- content: note.text ?? undefined,
+ content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined,
image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined,
});
}
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 396536948e..4275dc9527 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -29,7 +29,8 @@
let forceError = localStorage.getItem('forceError');
if (forceError != null) {
- renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
+ renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
+ return;
}
//#region Detect language & fetch translations
@@ -155,7 +156,12 @@
document.head.appendChild(css);
}
- function renderError(code, details) {
+ async function renderError(code, details) {
+ // Cannot set property 'innerHTML' of null を回避
+ if (document.readyState === 'loading') {
+ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
+ }
+
let errorsElement = document.getElementById('errors');
if (!errorsElement) {
@@ -314,6 +320,6 @@
#errorInfo {
width: 50%;
}
- `)
+ }`)
}
})();
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 929070d0d2..ecbbee4eff 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -90,6 +90,12 @@ export const moderationLogTypes = [
'deleteAvatarDecoration',
'unsetUserAvatar',
'unsetUserBanner',
+ 'createSystemWebhook',
+ 'updateSystemWebhook',
+ 'deleteSystemWebhook',
+ 'createAbuseReportNotificationRecipient',
+ 'updateAbuseReportNotificationRecipient',
+ 'deleteAbuseReportNotificationRecipient',
] as const;
export type ModerationLogPayloads = {
@@ -282,6 +288,32 @@ export type ModerationLogPayloads = {
userHost: string | null;
fileId: string;
};
+ createSystemWebhook: {
+ systemWebhookId: string;
+ webhook: any;
+ };
+ updateSystemWebhook: {
+ systemWebhookId: string;
+ before: any;
+ after: any;
+ };
+ deleteSystemWebhook: {
+ systemWebhookId: string;
+ webhook: any;
+ };
+ createAbuseReportNotificationRecipient: {
+ recipientId: string;
+ recipient: any;
+ };
+ updateAbuseReportNotificationRecipient: {
+ recipientId: string;
+ before: any;
+ after: any;
+ };
+ deleteAbuseReportNotificationRecipient: {
+ recipientId: string;
+ recipient: any;
+ };
};
export type Serialized<T> = {