From 8228434cf2bcd9ca7f2484db653a66d7ff59cc20 Mon Sep 17 00:00:00 2001 From: Jaehong Kang Date: Sun, 3 May 2026 12:29:18 +0900 Subject: [PATCH] feat: deliver Like activity to relays Like / Undo Like ActivityPub activities for public notes are now delivered to relay servers, matching the pattern already used by NoteCreateService, NoteDeleteService, PollService, and others for public-content activities. Introduces an admin meta option `deliverReactionsToRelays` (default true) so operators can opt out, since reactions occur far more frequently than notes and may significantly amplify load on relays and downstream servers. Closes #17307 --- CHANGELOG.md | 1 + locales/en-US.yml | 2 ++ locales/ja-JP.yml | 2 ++ locales/ko-KR.yml | 2 ++ .../1777776758782-deliverReactionsToRelays.js | 16 ++++++++++++++ packages/backend/src/core/ReactionService.ts | 11 ++++++++++ packages/backend/src/models/Meta.ts | 5 +++++ .../src/server/api/endpoints/admin/meta.ts | 5 +++++ .../server/api/endpoints/admin/update-meta.ts | 5 +++++ packages/frontend/src/pages/admin/relays.vue | 22 +++++++++++++++++++ packages/i18n/src/autogen/locale.ts | 8 +++++++ packages/misskey-js/src/autogen/types.ts | 2 ++ 12 files changed, 81 insertions(+) create mode 100644 packages/backend/migration/1777776758782-deliverReactionsToRelays.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 528d52cb206..34130b37061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - ### Server +- Feat: 公開ノートへのリアクション(Like)アクティビティをリレーサーバーへ配信するように(管理者設定 `deliverReactionsToRelays` で無効化可能、デフォルト有効) #17307 - Fix: ID生成アルゴリズムにULIDを使用している場合に通知が約10秒遅延する問題を修正 diff --git a/locales/en-US.yml b/locales/en-US.yml index a9729b2ce38..ab675b0969a 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1731,6 +1731,8 @@ _serverSettings: fanoutTimelineDbFallback: "Fallback to database" fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved." reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase." + deliverReactionsToRelays: "Deliver reactions to relays" + deliverReactionsToRelaysDescription: "When enabled, Like activities on public notes are also delivered to relay servers, so federated servers receiving notes via relay will also receive reactions. Reactions are more frequent than notes, so this can be disabled to reduce the load on relays and downstream servers." remoteNotesCleaning: "Automatic cleanup of remote notes" remoteNotesCleaning_description: "When enabled, unused and outdated remote notes will be periodically cleaned up to prevent database bloat." remoteNotesCleaningMaxProcessingDuration: "Maximum cleanup processing time" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 93679aa24b6..5cf47cec9a3 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1752,6 +1752,8 @@ _serverSettings: fanoutTimelineDbFallback: "データベースへのフォールバック" fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。" + deliverReactionsToRelays: "リアクションをリレーに配信する" + deliverReactionsToRelaysDescription: "有効にすると、公開ノートへのリアクション(Like)アクティビティをリレーサーバーへも配信します。リレー経由でノートを取得している連合サーバーがリアクションも受信できるようになります。リアクションは投稿より頻度が高いため、リレーや配信先の負荷を考慮して無効化することもできます。" remoteNotesCleaning: "リモート投稿の自動クリーニング" remoteNotesCleaning_description: "有効にすると、一定期間経過したリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。" remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 294791cce31..81f6bf4dc7b 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1732,6 +1732,8 @@ _serverSettings: fanoutTimelineDbFallback: "데이터베이스를 예비로 사용하기" fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해 DB에 질의하여 정보를 가져옵니다. 비활성화하면 이를 실행하지 않음으로써 서버의 부하를 줄일 수 있지만, 타임라인에서 가져올 수 있는 게시물 범위가 한정됩니다." reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다." + deliverReactionsToRelays: "리액션을 릴레이에 배포" + deliverReactionsToRelaysDescription: "활성화하면 공개 노트에 달린 리액션(Like) 액티비티를 릴레이 서버에도 배포합니다. 릴레이를 통해 노트를 받고 있는 연합 서버가 리액션도 수신할 수 있게 됩니다. 리액션은 노트보다 빈도가 높으므로, 릴레이나 배포 대상 서버의 부하를 고려하여 비활성화할 수 있습니다." remoteNotesCleaning: "리모트 서버 노트 자동 정리 " remoteNotesCleaning_description: "더 이상 사용되지 않는 오래된 리모트 노트를 정기적으로 정리하여, 데이터 베이스의 사용량을 절약할 수 있습니다." remoteNotesCleaningMaxProcessingDuration: "리모트 노트 자동 정리 최대 실행 시간" diff --git a/packages/backend/migration/1777776758782-deliverReactionsToRelays.js b/packages/backend/migration/1777776758782-deliverReactionsToRelays.js new file mode 100644 index 00000000000..2f5c3c5e782 --- /dev/null +++ b/packages/backend/migration/1777776758782-deliverReactionsToRelays.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class DeliverReactionsToRelays1777776758782 { + name = 'DeliverReactionsToRelays1777776758782' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "deliverReactionsToRelays" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deliverReactionsToRelays"`); + } +} diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index cd1e87dbd8e..1471b9cfd75 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -17,6 +17,7 @@ import { NotificationService } from '@/core/NotificationService.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; import { emojiRegex } from '@/misc/emoji-regex.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; +import { RelayService } from '@/core/RelayService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -97,6 +98,7 @@ export class ReactionService { private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, + private relayService: RelayService, private notificationService: NotificationService, private perUserReactionsChart: PerUserReactionsChart, ) { @@ -280,6 +282,10 @@ export class ReactionService { } } + if (note.visibility === 'public' && this.meta.deliverReactionsToRelays) { + this.relayService.deliverToRelays(user, content); + } + trackPromise(dm.execute()); } //#endregion @@ -332,6 +338,11 @@ export class ReactionService { dm.addDirectRecipe(reactee as MiRemoteUser); } dm.addFollowersRecipe(); + + if (note.visibility === 'public' && this.meta.deliverReactionsToRelays) { + this.relayService.deliverToRelays(user, content); + } + trackPromise(dm.execute()); } //#endregion diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 620853450cd..0699be78bbc 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -609,6 +609,11 @@ export class MiMeta { }) public enableReactionsBuffering: boolean; + @Column('boolean', { + default: true, + }) + public deliverReactionsToRelays: boolean; + @Column('integer', { default: 0, }) diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 5beed3a7e8b..309e2ca0279 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -403,6 +403,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + deliverReactionsToRelays: { + type: 'boolean', + optional: false, nullable: false, + }, notesPerOneAd: { type: 'number', optional: false, nullable: false, @@ -731,6 +735,7 @@ export default class extends Endpoint { // eslint- perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, enableReactionsBuffering: instance.enableReactionsBuffering, + deliverReactionsToRelays: instance.deliverReactionsToRelays, notesPerOneAd: instance.notesPerOneAd, summalyProxy: instance.urlPreviewSummaryProxyUrl, urlPreviewEnabled: instance.urlPreviewEnabled, 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 372fe3a25f5..041b73621f6 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -159,6 +159,7 @@ export const paramDef = { perUserHomeTimelineCacheMax: { type: 'integer' }, perUserListTimelineCacheMax: { type: 'integer' }, enableReactionsBuffering: { type: 'boolean' }, + deliverReactionsToRelays: { type: 'boolean' }, notesPerOneAd: { type: 'integer' }, silencedHosts: { type: 'array', @@ -676,6 +677,10 @@ export default class extends Endpoint { // eslint- set.enableReactionsBuffering = ps.enableReactionsBuffering; } + if (ps.deliverReactionsToRelays !== undefined) { + set.deliverReactionsToRelays = ps.deliverReactionsToRelays; + } + if (ps.notesPerOneAd !== undefined) { set.notesPerOneAd = ps.notesPerOneAd; } diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue index 3526e036d35..78a17590c27 100644 --- a/packages/frontend/src/pages/admin/relays.vue +++ b/packages/frontend/src/pages/admin/relays.vue @@ -8,6 +8,15 @@ SPDX-License-Identifier: AGPL-3.0-only
+ +
+ + + + +
+
+
{{ relay.inbox }}
@@ -28,12 +37,25 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; +import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +const meta = await misskeyApi('admin/meta'); + const relays = ref([]); +const deliverReactionsToRelays = ref(meta.deliverReactionsToRelays); + +function onChange_deliverReactionsToRelays(value: boolean) { + os.apiWithDialog('admin/update-meta', { + deliverReactionsToRelays: value, + }).then(() => { + fetchInstance(true); + }); +} async function addRelay() { const { canceled, result: inbox } = await os.inputText({ diff --git a/packages/i18n/src/autogen/locale.ts b/packages/i18n/src/autogen/locale.ts index 69e7346a596..1df0a759701 100644 --- a/packages/i18n/src/autogen/locale.ts +++ b/packages/i18n/src/autogen/locale.ts @@ -6868,6 +6868,14 @@ export interface Locale extends ILocale { * 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。 */ "reactionsBufferingDescription": string; + /** + * リアクションをリレーに配信する + */ + "deliverReactionsToRelays": string; + /** + * 有効にすると、公開ノートへのリアクション(Like)アクティビティをリレーサーバーへも配信します。リレー経由でノートを取得している連合サーバーがリアクションも受信できるようになります。リアクションは投稿より頻度が高いため、リレーや配信先の負荷を考慮して無効化することもできます。 + */ + "deliverReactionsToRelaysDescription": string; /** * リモート投稿の自動クリーニング */ diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index df08b3f8047..6bc2bf4fde6 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -9472,6 +9472,7 @@ export interface operations { perUserHomeTimelineCacheMax: number; perUserListTimelineCacheMax: number; enableReactionsBuffering: boolean; + deliverReactionsToRelays: boolean; notesPerOneAd: number; backgroundImageUrl: string | null; deeplAuthKey: string | null; @@ -12821,6 +12822,7 @@ export interface operations { perUserHomeTimelineCacheMax?: number; perUserListTimelineCacheMax?: number; enableReactionsBuffering?: boolean; + deliverReactionsToRelays?: boolean; notesPerOneAd?: number; silencedHosts?: string[] | null; mediaSilencedHosts?: string[] | null;