From 2834e54e78c5ca1932aae1661e497aa8c899aab0 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:10:41 +0900 Subject: fix(backend): make token nullable (#12280) --- packages/backend/src/server/api/endpoints/i/revoke-token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index e8bb282533..98d866f867 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -18,7 +18,7 @@ export const paramDef = { type: 'object', properties: { tokenId: { type: 'string', format: 'misskey:id' }, - token: { type: 'string' }, + token: { type: 'string', nullable: true }, }, anyOf: [ { required: ['tokenId'] }, -- cgit v1.2.3-freya From e1cc95d3084cefe693a1f7028848d1c8aaceacf6 Mon Sep 17 00:00:00 2001 From: おさむのひと <46447427+samunohito@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:27:37 +0900 Subject: ユーザのノートのみ表示時にDBへフォールバックするとリノートを含んでしまうのを修正 (#12321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ユーザのノートのみ表示時にDBへフォールバックするとリノートを含んでしまうのを修正 * fix CHANGELOG.md --------- Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com> --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints/users/notes.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/CHANGELOG.md b/CHANGELOG.md index 57415554e7..6f9e540fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ ### Server - Fix: トークンのないプラグインをアンインストールするときにエラーが出ないように - Fix: 投稿通知がオンでもダイレクト投稿はユーザーに通知されないようにされました +- Fix: ユーザタイムラインの「ノート」選択時にリノートが混ざり込んでしまうことがある問題の修正 #12306 ## 2023.11.0 diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 343d320f6e..a43e572922 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -51,7 +51,6 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, - includeMyRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, excludeNsfw: { type: 'boolean', default: false }, }, @@ -169,7 +168,7 @@ export default class extends Endpoint { // eslint- query.andWhere('note.fileIds != \'{}\''); } - if (ps.includeMyRenotes === false) { + if (ps.withRenotes === false) { query.andWhere(new Brackets(qb => { qb.orWhere('note.userId != :userId', { userId: ps.userId }); qb.orWhere('note.renoteId IS NULL'); -- cgit v1.2.3-freya From 2d2eefe3d44ad7118bbe39d5acb1301ed802522e Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 14 Nov 2023 07:58:18 +0900 Subject: feat: メールアドレスを使用してユーザー名を割り出す機能 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #10158 --- CHANGELOG.md | 2 +- packages/backend/src/server/api/EndpointsModule.ts | 4 ++ packages/backend/src/server/api/endpoints.ts | 2 + .../api/endpoints/admin/accounts/find-by-email.ts | 61 ++++++++++++++++++++++ packages/frontend/src/pages/admin/index.vue | 16 ++++-- packages/frontend/src/scripts/lookup-user.ts | 23 ++++++++ 6 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts (limited to 'packages/backend/src/server/api') diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d89e2fc7..c286fc3efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ ## 2023.x.x (unreleased) ### General -- +- Feat: コントロールパネルの「照会」から、入力されたメールアドレスを持つユーザーを検索できるようになりました ### Client - Fix: アイコンデコレーションが複数の場所で見切れている問題を修正 diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 23067a9b26..6d813ae04e 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -10,6 +10,7 @@ import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.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'; import * as ep___admin_ad_create from './endpoints/admin/ad/create.js'; import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js'; import * as ep___admin_ad_list from './endpoints/admin/ad/list.js'; @@ -368,6 +369,7 @@ const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_m const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.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 }; const $admin_ad_create: Provider = { provide: 'ep:admin/ad/create', useClass: ep___admin_ad_create.default }; const $admin_ad_delete: Provider = { provide: 'ep:admin/ad/delete', useClass: ep___admin_ad_delete.default }; const $admin_ad_list: Provider = { provide: 'ep:admin/ad/list', useClass: ep___admin_ad_list.default }; @@ -730,6 +732,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_abuseUserReports, $admin_accounts_create, $admin_accounts_delete, + $admin_accounts_findByEmail, $admin_ad_create, $admin_ad_delete, $admin_ad_list, @@ -1086,6 +1089,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_abuseUserReports, $admin_accounts_create, $admin_accounts_delete, + $admin_accounts_findByEmail, $admin_ad_create, $admin_ad_delete, $admin_ad_list, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index af798fd166..ef4bd3e2b5 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -10,6 +10,7 @@ import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.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'; import * as ep___admin_ad_create from './endpoints/admin/ad/create.js'; import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js'; import * as ep___admin_ad_list from './endpoints/admin/ad/list.js'; @@ -366,6 +367,7 @@ const eps = [ ['admin/abuse-user-reports', ep___admin_abuseUserReports], ['admin/accounts/create', ep___admin_accounts_create], ['admin/accounts/delete', ep___admin_accounts_delete], + ['admin/accounts/find-by-email', ep___admin_accounts_findByEmail], ['admin/ad/create', ep___admin_ad_create], ['admin/ad/delete', ep___admin_ad_delete], ['admin/ad/list', ep___admin_ad_list], diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts new file mode 100644 index 0000000000..686341582b --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserProfilesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireAdmin: true, + + errors: { + userNotFound: { + message: 'No such user who has the email address.', + code: 'USER_NOT_FOUND', + id: 'cb865949-8af5-4062-a88c-ef55e8786d1d', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + email: { type: 'string' }, + }, + required: ['email'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOne({ + where: { email: ps.email }, + relations: ['user'], + }); + + if (profile == null) { + throw new ApiError(meta.errors.userNotFound); + } + + const res = await this.userEntityService.pack(profile.user!, null, { + detail: true, + }); + + return res; + }); + } +} diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index b304edbf57..cc0cdf7466 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -34,7 +34,7 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue'; import MkInfo from '@/components/MkInfo.vue'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; -import { lookupUser } from '@/scripts/lookup-user.js'; +import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js'; import { useRouter } from '@/router.js'; import { definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; @@ -264,7 +264,7 @@ provideMetadataReceiver((info) => { } }); -const invite = () => { +function invite() { os.api('admin/invite/create').then(x => { os.alert({ type: 'info', @@ -276,15 +276,21 @@ const invite = () => { text: err, }); }); -}; +} -const lookup = (ev) => { +function lookup(ev: MouseEvent) { os.popupMenu([{ text: i18n.ts.user, icon: 'ti ti-user', action: () => { lookupUser(); }, + }, { + text: `${i18n.ts.user} (${i18n.ts.email})`, + icon: 'ti ti-user', + action: () => { + lookupUserByEmail(); + }, }, { text: i18n.ts.note, icon: 'ti ti-pencil', @@ -304,7 +310,7 @@ const lookup = (ev) => { alert('TODO'); }, }], ev.currentTarget ?? ev.target); -}; +} const headerActions = $computed(() => []); diff --git a/packages/frontend/src/scripts/lookup-user.ts b/packages/frontend/src/scripts/lookup-user.ts index 3dbc03f777..a35fe898e4 100644 --- a/packages/frontend/src/scripts/lookup-user.ts +++ b/packages/frontend/src/scripts/lookup-user.ts @@ -39,3 +39,26 @@ export async function lookupUser() { notFound(); }); } + +export async function lookupUserByEmail() { + const { canceled, result } = await os.inputText({ + title: i18n.ts.emailAddress, + type: 'email', + }); + if (canceled) return; + + try { + const user = await os.apiWithDialog('admin/accounts/find-by-email', { email: result }); + + os.pageWindow(`/admin/user/${user.id}`); + } catch (err) { + if (err.code === 'USER_NOT_FOUND') { + os.alert({ + type: 'error', + text: i18n.ts.noSuchUser, + }); + } else { + throw err; + } + } +} -- cgit v1.2.3-freya From aa6d0d4359d58a8295c60a79c44ab0dfe094b04e Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:10:45 +0900 Subject: fix(backend): 非公開の投稿に対して返信できないように (#12333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): 非公開の投稿に対して返信できないように * Update CHANGELOG.md * fix: test --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints/notes/create.ts | 8 ++++++++ packages/backend/test/e2e/timelines.ts | 1 + 3 files changed, 10 insertions(+) (limited to 'packages/backend/src/server/api') diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e32249ee7..c08cb036f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Fix: 投稿通知がオンでもダイレクト投稿はユーザーに通知されないようにされました - Fix: ユーザタイムラインの「ノート」選択時にリノートが混ざり込んでしまうことがある問題の修正 #12306 - Fix: ActivityPubに関するセキュリティの向上 +- Fix: 非公開の投稿に対して返信できないように ## 2023.11.0 diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index df02d3acb7..513b77b98a 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -70,6 +70,12 @@ export const meta = { id: '749ee0f6-d3da-459a-bf02-282e2da4292c', }, + cannotReplyToInvisibleNote: { + message: 'You cannot reply to an invisible Note.', + code: 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE', + id: 'b98980fa-3780-406c-a935-b6d0eeee10d1', + }, + cannotReplyToPureRenote: { message: 'You can not reply to a pure Renote.', code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', @@ -276,6 +282,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchReplyTarget); } else if (isPureRenote(reply)) { throw new ApiError(meta.errors.cannotReplyToPureRenote); + } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) { + throw new ApiError(meta.errors.cannotReplyToInvisibleNote); } // Check blocking diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 760bb8a574..73c446444b 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -152,6 +152,7 @@ describe('Timelines', () => { await api('/following/create', { userId: bob.id }, alice); await api('/following/create', { userId: carol.id }, alice); + await api('/following/create', { userId: carol.id }, bob); await api('/following/update', { userId: bob.id, withReplies: true }, alice); await sleep(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); -- cgit v1.2.3-freya From ca81f0ddbbeea1e5ebd95ae81a6016c7a57612fc Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 15 Nov 2023 16:17:21 +0900 Subject: fix(backend): 追加情報のカスタム絵文字がユーザー情報のtagに含まれない問題を修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #12316 --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints/i/update.ts | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/CHANGELOG.md b/CHANGELOG.md index fc6c8ce711..d872ff153b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Fix: トークンのないプラグインをアンインストールするときにエラーが出ないように - Fix: 投稿通知がオンでもダイレクト投稿はユーザーに通知されないようにされました - Fix: ユーザタイムラインの「ノート」選択時にリノートが混ざり込んでしまうことがある問題の修正 #12306 +- Fix: ActivityPub: 追加情報のカスタム絵文字がユーザー情報のtagに含まれない問題を修正 - Fix: ActivityPubに関するセキュリティの向上 - Fix: 非公開の投稿に対して返信できないように diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 0e6a4d2e36..b00aa87bee 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -379,16 +379,26 @@ export default class extends Endpoint { // eslint- const newName = updates.name === undefined ? user.name : updates.name; const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; + const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; if (newName != null) { const tokens = mfm.parseSimple(newName); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens)); } if (newDescription != null) { const tokens = mfm.parse(newDescription); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); - tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens)); + tags = extractHashtags(tokens).map(tag => normalizeForSearch(tag)).splice(0, 32); + } + + for (const field of newFields) { + const nameTokens = mfm.parseSimple(field.name); + const valueTokens = mfm.parseSimple(field.value); + emojis = emojis.concat([ + ...extractCustomEmojisFromMfm(nameTokens), + ...extractCustomEmojisFromMfm(valueTokens), + ]); } updates.emojis = emojis; -- cgit v1.2.3-freya From 9d78a1a8b3c17e7f91b85e68d03502c068dd6c97 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 16 Nov 2023 10:20:57 +0900 Subject: enhance(backend): make ftt db fallback configurable --- CHANGELOG.md | 1 + locales/index.d.ts | 2 + locales/ja-JP.yml | 2 + ...1700096812223-enableFanoutTimelineDbFallback.js | 16 ++ packages/backend/src/models/Meta.ts | 5 + .../backend/src/server/api/endpoints/admin/meta.ts | 5 + .../src/server/api/endpoints/admin/update-meta.ts | 5 + .../server/api/endpoints/notes/hybrid-timeline.ts | 166 +++++++++-------- .../server/api/endpoints/notes/local-timeline.ts | 138 +++++++------- .../src/server/api/endpoints/notes/timeline.ts | 134 ++++++------- .../api/endpoints/notes/user-list-timeline.ts | 207 +++++++++++++-------- packages/backend/test/unit/activitypub.ts | 1 + packages/frontend/src/pages/admin/settings.vue | 8 + 13 files changed, 395 insertions(+), 295 deletions(-) create mode 100644 packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js (limited to 'packages/backend/src/server/api') diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8b9fac76..4c4ae2ae3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Fix: 特定の条件下でノートがnyaizeされない問題を修正 ### Server +- Enhance: FTTのデータベースへのフォールバック処理を行うかどうかを設定可能に - Fix: トークンのないプラグインをアンインストールするときにエラーが出ないように - Fix: 投稿通知がオンでもダイレクト投稿はユーザーに通知されないようにされました - Fix: ユーザタイムラインの「ノート」選択時にリノートが混ざり込んでしまうことがある問題の修正 #12306 diff --git a/locales/index.d.ts b/locales/index.d.ts index fc6653b05b..968334e31b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1285,6 +1285,8 @@ export interface Locale { "shortName": string; "shortNameDescription": string; "fanoutTimelineDescription": string; + "fanoutTimelineDbFallback": string; + "fanoutTimelineDbFallbackDescription": string; }; "_accountMigration": { "moveFrom": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 67a57f994c..5b1d0d62bd 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1272,6 +1272,8 @@ _serverSettings: shortName: "略称" shortNameDescription: "サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。" fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。" + fanoutTimelineDbFallback: "データベースへのフォールバック" + fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" _accountMigration: moveFrom: "別のアカウントからこのアカウントに移行" diff --git a/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js b/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js new file mode 100644 index 0000000000..94fa588985 --- /dev/null +++ b/packages/backend/migration/1700096812223-enableFanoutTimelineDbFallback.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class EnableFanoutTimelineDbFallback1700096812223 { + name = 'EnableFanoutTimelineDbFallback1700096812223' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableFanoutTimelineDbFallback" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFanoutTimelineDbFallback"`); + } +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 360239f509..14a72add1d 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -494,6 +494,11 @@ export class MiMeta { }) public enableFanoutTimeline: boolean; + @Column('boolean', { + default: true, + }) + public enableFanoutTimelineDbFallback: boolean; + @Column('integer', { default: 300, }) diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 73c84a8674..cc9afaf7fd 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -295,6 +295,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + enableFanoutTimelineDbFallback: { + type: 'boolean', + optional: false, nullable: false, + }, perLocalUserUserTimelineCacheMax: { type: 'number', optional: false, nullable: false, @@ -424,6 +428,7 @@ export default class extends Endpoint { // eslint- policies: { ...DEFAULT_POLICIES, ...instance.policies }, manifestJsonOverride: instance.manifestJsonOverride, enableFanoutTimeline: instance.enableFanoutTimeline, + enableFanoutTimelineDbFallback: instance.enableFanoutTimelineDbFallback, perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax, perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, 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 c58569a31c..da3e5dd9ac 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -121,6 +121,7 @@ export const paramDef = { preservedUsernames: { type: 'array', items: { type: 'string' } }, manifestJsonOverride: { type: 'string' }, enableFanoutTimeline: { type: 'boolean' }, + enableFanoutTimelineDbFallback: { type: 'boolean' }, perLocalUserUserTimelineCacheMax: { type: 'integer' }, perRemoteUserUserTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' }, @@ -485,6 +486,10 @@ export default class extends Endpoint { // eslint- set.enableFanoutTimeline = ps.enableFanoutTimeline; } + if (ps.enableFanoutTimelineDbFallback !== undefined) { + set.enableFanoutTimelineDbFallback = ps.enableFanoutTimelineDbFallback; + } + if (ps.perLocalUserUserTimelineCacheMax !== undefined) { set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax; } 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 19c24a78f4..408c2fa371 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -93,87 +93,100 @@ export default class extends Endpoint { // eslint- const serverSettings = await this.metaService.fetch(); - if (serverSettings.enableFanoutTimeline) { - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]); - - let noteIds: string[]; - let shouldFallbackToDb = false; - - if (ps.withFiles) { - const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ - `homeTimelineWithFiles:${me.id}`, - 'localTimelineWithFiles', - ], untilId, sinceId); - noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); - } else if (ps.withReplies) { - const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([ - `homeTimeline:${me.id}`, - 'localTimeline', - 'localTimelineWithReplies', - ], untilId, sinceId); - noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds])); - } else { - const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ - `homeTimeline:${me.id}`, - 'localTimeline', - ], untilId, sinceId); - noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); - shouldFallbackToDb = htlNoteIds.length === 0; - } + if (!serverSettings.enableFanoutTimeline) { + return await this.getFromDb({ + untilId, + sinceId, + limit: ps.limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withReplies: ps.withReplies, + }, me); + } - noteIds.sort((a, b) => a > b ? -1 : 1); - noteIds = noteIds.slice(0, ps.limit); + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]); + + let noteIds: string[]; + let shouldFallbackToDb = false; + + if (ps.withFiles) { + const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ + `homeTimelineWithFiles:${me.id}`, + 'localTimelineWithFiles', + ], untilId, sinceId); + noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); + } else if (ps.withReplies) { + const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([ + `homeTimeline:${me.id}`, + 'localTimeline', + 'localTimelineWithReplies', + ], untilId, sinceId); + noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds])); + } else { + const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ + `homeTimeline:${me.id}`, + 'localTimeline', + ], untilId, sinceId); + noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); + shouldFallbackToDb = htlNoteIds.length === 0; + } - shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); + noteIds.sort((a, b) => a > b ? -1 : 1); + noteIds = noteIds.slice(0, ps.limit); - let redisTimeline: MiNote[] = []; + shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); - if (!shouldFallbackToDb) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); + let redisTimeline: MiNote[] = []; - redisTimeline = await query.getMany(); + if (!shouldFallbackToDb) { + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - redisTimeline = redisTimeline.filter(note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } + redisTimeline = await query.getMany(); + redisTimeline = redisTimeline.filter(note => { + if (note.userId === me.id) { return true; - }); + } + if (isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } + } - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } + return true; + }); - if (redisTimeline.length > 0) { - process.nextTick(() => { - this.activeUsersChart.read(me); - }); + redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); + } - return await this.noteEntityService.packMany(redisTimeline, me); - } else { // fallback to db + if (redisTimeline.length > 0) { + process.nextTick(() => { + this.activeUsersChart.read(me); + }); + + return await this.noteEntityService.packMany(redisTimeline, me); + } else { + if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db return await this.getFromDb({ untilId, sinceId, @@ -184,18 +197,9 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, }, me); + } else { + return []; } - } else { - return await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - }, me); } }); } diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 94a640e70a..003dae6614 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -84,75 +84,85 @@ export default class extends Endpoint { // eslint- const serverSettings = await this.metaService.fetch(); - if (serverSettings.enableFanoutTimeline) { - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]) : [new Set(), new Set(), new Set()]; - - let noteIds: string[]; - - if (ps.withFiles) { - noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId); - } else { - const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([ - 'localTimeline', - 'localTimelineWithReplies', - ], untilId, sinceId); - noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds])); - noteIds.sort((a, b) => a > b ? -1 : 1); - } + if (!serverSettings.enableFanoutTimeline) { + return await this.getFromDb({ + untilId, + sinceId, + limit: ps.limit, + withFiles: ps.withFiles, + withReplies: ps.withReplies, + }, me); + } - noteIds = noteIds.slice(0, ps.limit); + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]) : [new Set(), new Set(), new Set()]; - let redisTimeline: MiNote[] = []; + let noteIds: string[]; - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); + if (ps.withFiles) { + noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId); + } else { + const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([ + 'localTimeline', + 'localTimelineWithReplies', + ], untilId, sinceId); + noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds])); + noteIds.sort((a, b) => a > b ? -1 : 1); + } - redisTimeline = await query.getMany(); + noteIds = noteIds.slice(0, ps.limit); - redisTimeline = redisTimeline.filter(note => { - if (me && (note.userId === me.id)) { - return true; - } - if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false; - if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } + let redisTimeline: MiNote[] = []; - return true; - }); + if (noteIds.length > 0) { + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } + redisTimeline = await query.getMany(); - if (redisTimeline.length > 0) { - process.nextTick(() => { - if (me) { - this.activeUsersChart.read(me); + redisTimeline = redisTimeline.filter(note => { + if (me && (note.userId === me.id)) { + return true; + } + if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false; + if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; } - }); + } + + return true; + }); + + redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); + } - return await this.noteEntityService.packMany(redisTimeline, me); - } else { // fallback to db + if (redisTimeline.length > 0) { + process.nextTick(() => { + if (me) { + this.activeUsersChart.read(me); + } + }); + + return await this.noteEntityService.packMany(redisTimeline, me); + } else { + if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db return await this.getFromDb({ untilId, sinceId, @@ -160,15 +170,9 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, }, me); + } else { + return []; } - } else { - return await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - }, me); } }); } diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 5016bd3acb..8037d4862f 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -76,65 +76,78 @@ export default class extends Endpoint { // eslint- const serverSettings = await this.metaService.fetch(); - if (serverSettings.enableFanoutTimeline) { - const [ - followings, - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(me.id), - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]); - - let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); - - let redisTimeline: MiNote[] = []; - - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - redisTimeline = await query.getMany(); - - redisTimeline = redisTimeline.filter(note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - if (note.reply && note.reply.visibility === 'followers') { - if (!Object.hasOwn(followings, note.reply.userId)) return false; - } + if (!serverSettings.enableFanoutTimeline) { + return await this.getFromDb({ + untilId, + sinceId, + limit: ps.limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me); + } + const [ + followings, + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = await Promise.all([ + this.cacheService.userFollowingsCache.fetch(me.id), + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]); + + let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId); + noteIds = noteIds.slice(0, ps.limit); + + let redisTimeline: MiNote[] = []; + + if (noteIds.length > 0) { + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + redisTimeline = await query.getMany(); + + redisTimeline = redisTimeline.filter(note => { + if (note.userId === me.id) { return true; - }); + } + if (isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } + } + if (note.reply && note.reply.visibility === 'followers') { + if (!Object.hasOwn(followings, note.reply.userId)) return false; + } - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } + return true; + }); + + redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); + } - if (redisTimeline.length > 0) { - process.nextTick(() => { - this.activeUsersChart.read(me); - }); + if (redisTimeline.length > 0) { + process.nextTick(() => { + this.activeUsersChart.read(me); + }); - return await this.noteEntityService.packMany(redisTimeline, me); - } else { // fallback to db + return await this.noteEntityService.packMany(redisTimeline, me); + } else { + if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db return await this.getFromDb({ untilId, sinceId, @@ -145,18 +158,9 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, }, me); + } else { + return []; } - } else { - return await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me); } }); } diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 9ead1410c2..dbc3875597 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -4,7 +4,8 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { MiNote, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import { Brackets } from 'typeorm'; +import type { MiNote, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -14,8 +15,9 @@ import { IdService } from '@/core/IdService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; +import { MiLocalUser } from '@/models/User.js'; +import { MetaService } from '@/core/MetaService.js'; import { ApiError } from '../../error.js'; -import { Brackets } from 'typeorm'; export const meta = { tags: ['notes', 'lists'], @@ -81,7 +83,7 @@ export default class extends Endpoint { // eslint- private idService: IdService, private funoutTimelineService: FunoutTimelineService, private queryService: QueryService, - + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -96,6 +98,21 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchList); } + const serverSettings = await this.metaService.fetch(); + + if (!serverSettings.enableFanoutTimeline) { + return await this.getFromDb(list, { + untilId, + sinceId, + limit: ps.limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me); + } + const [ userIdsWhoMeMuting, userIdsWhoMeMutingRenotes, @@ -145,93 +162,119 @@ export default class extends Endpoint { // eslint- if (redisTimeline.length > 0) { this.activeUsersChart.read(me); return await this.noteEntityService.packMany(redisTimeline, me); - } else { // fallback to db - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .andWhere('userListMemberships.userListId = :userListId', { userListId: list.id }) - .andWhere('note.channelId IS NULL') // チャンネルノートではない - .andWhere(new Brackets(qb => { - qb - .where('note.replyId IS NULL') // 返信ではない - .orWhere(new Brackets(qb => { - qb // 返信だけど投稿者自身への返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = note.userId'); - })) - .orWhere(new Brackets(qb => { - qb // 返信だけど自分宛ての返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = :meId', { meId: me.id }); - })) - .orWhere(new Brackets(qb => { - qb // 返信だけどwithRepliesがtrueの場合 - .where('note.replyId IS NOT NULL') - .andWhere('userListMemberships.withReplies = true'); - })); - })); - - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); + } else { + if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db + return await this.getFromDb(list, { + untilId, + sinceId, + limit: ps.limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me); + } else { + return []; } + } + }); + } - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + private async getFromDb(list: MiUserList, ps: { + untilId: string | null, + sinceId: string | null, + limit: number, + includeMyRenotes: boolean, + includeRenotedMyNotes: boolean, + includeLocalRenotes: boolean, + withFiles: boolean, + withRenotes: boolean, + }, me: MiLocalUser) { + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .andWhere('userListMemberships.userListId = :userListId', { userListId: list.id }) + .andWhere('note.channelId IS NULL') // チャンネルノートではない + .andWhere(new Brackets(qb => { + qb + .where('note.replyId IS NULL') // 返信ではない + .orWhere(new Brackets(qb => { + qb // 返信だけど投稿者自身への返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.replyUserId = note.userId'); + })) + .orWhere(new Brackets(qb => { + qb // 返信だけど自分宛ての返信 + .where('note.replyId IS NOT NULL') + .andWhere('note.replyUserId = :meId', { meId: me.id }); + })) + .orWhere(new Brackets(qb => { + qb // 返信だけどwithRepliesがtrueの場合 + .where('note.replyId IS NOT NULL') + .andWhere('userListMemberships.withReplies = true'); })); - } + })); - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } + if (ps.includeMyRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion + if (ps.includeRenotedMyNotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } - const timeline = await query.limit(ps.limit).getMany(); + if (ps.includeLocalRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserHost IS NOT NULL'); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } - this.activeUsersChart.read(me); + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } - return await this.noteEntityService.packMany(timeline, me); - } - }); + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion + + const timeline = await query.limit(ps.limit).getMany(); + + this.activeUsersChart.read(me); + + return await this.noteEntityService.packMany(timeline, me); } } diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 832d1f490f..63952e6434 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -94,6 +94,7 @@ describe('ActivityPub', () => { cacheRemoteFiles: true, cacheRemoteSensitiveFiles: true, enableFanoutTimeline: true, + enableFanoutTimelineDbFallback: true, perUserHomeTimelineCacheMax: 100, perLocalUserUserTimelineCacheMax: 100, perRemoteUserUserTimelineCacheMax: 100, diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index a15be25620..86fbfa0827 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -95,6 +95,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + @@ -171,6 +176,7 @@ let enableServiceWorker: boolean = $ref(false); let swPublicKey: any = $ref(null); let swPrivateKey: any = $ref(null); let enableFanoutTimeline: boolean = $ref(false); +let enableFanoutTimelineDbFallback: boolean = $ref(false); let perLocalUserUserTimelineCacheMax: number = $ref(0); let perRemoteUserUserTimelineCacheMax: number = $ref(0); let perUserHomeTimelineCacheMax: number = $ref(0); @@ -192,6 +198,7 @@ async function init(): Promise { swPublicKey = meta.swPublickey; swPrivateKey = meta.swPrivateKey; enableFanoutTimeline = meta.enableFanoutTimeline; + enableFanoutTimelineDbFallback = meta.enableFanoutTimelineDbFallback; perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax; perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax; perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax; @@ -214,6 +221,7 @@ async function save(): void { swPublicKey, swPrivateKey, enableFanoutTimeline, + enableFanoutTimelineDbFallback, perLocalUserUserTimelineCacheMax, perRemoteUserUserTimelineCacheMax, perUserHomeTimelineCacheMax, -- cgit v1.2.3-freya From 1eb769dbe8e52070f8b3e2e0411b4943bd7d69a9 Mon Sep 17 00:00:00 2001 From: おさむのひと <46447427+samunohito@users.noreply.github.com> Date: Thu, 16 Nov 2023 16:02:46 +0900 Subject: LTLに特定条件下でチャンネル投稿が混ざり込む現象の修正 (#12347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LTLにチャンネル投稿を含まないように修正 * fix CHANGELOG.md --------- Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com> --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints/notes/local-timeline.ts | 2 +- packages/backend/src/server/api/stream/channels/local-timeline.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c4ae2ae3a..a87de8f662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Fix: ActivityPub: 追加情報のカスタム絵文字がユーザー情報のtagに含まれない問題を修正 - Fix: ActivityPubに関するセキュリティの向上 - Fix: 非公開の投稿に対して返信できないように +- Fix: LTLに特定条件下にてチャンネルへの投稿が混ざり込む現象を修正 ## 2023.11.0 diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 003dae6614..79baa6b285 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -186,7 +186,7 @@ export default class extends Endpoint { // eslint- }, me: MiLocalUser | null) { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') + .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL) AND (note.channelId IS NULL)') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') 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 9dd05b9b08..1388f186ff 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -52,7 +52,7 @@ class LocalTimelineChannel extends Channel { if (note.user.host !== null) return; if (note.visibility !== 'public') return; - if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; + if (note.channelId != null) return; // 関係ない返信は除外 if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) { -- cgit v1.2.3-freya