From ddd7b26f1c17e9ce1e0ea9961381d30979e6dc22 Mon Sep 17 00:00:00 2001 From: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:59:49 +0900 Subject: [PATCH 01/78] =?UTF-8?q?enhance(frontend):=20=E3=83=8E=E3=83=BC?= =?UTF-8?q?=E3=83=88=E4=BD=9C=E6=88=90=E7=94=BB=E9=9D=A2=E3=81=AE=E6=B7=BB?= =?UTF-8?q?=E4=BB=98=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC=E3=81=AEdivider?= =?UTF-8?q?=E3=81=AE=E4=BD=8D=E7=BD=AE=E3=82=92"=E6=B7=BB=E4=BB=98?= =?UTF-8?q?=E5=8F=96=E3=82=8A=E6=B6=88=E3=81=97"=E3=81=AE=E4=B8=8A?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B=20(#13409)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend): change divider position for MkPostFormAttaches * docs(changelog): update --- CHANGELOG.md | 1 + packages/frontend/src/components/MkPostFormAttaches.vue | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a91c3e43d4..fb4d1e7ad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### General ### Client +- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 - Fix: syuilo/misskeyの時代からあるインスタンスが改変されたバージョンであると誤認識される問題 - Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正 diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 3f775bc6e2..95eb367318 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -152,11 +152,11 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { icon: 'ti ti-crop', action: () : void => { crop(file); }, }] : [], { + type: 'divider', + }, { text: i18n.ts.attachCancel, icon: 'ti ti-circle-x', action: () => { detachMedia(file.id); }, - }, { - type: 'divider', }, { text: i18n.ts.deleteFile, icon: 'ti ti-trash', From 39c4e3a4f550b1383d574b9823b99a228b47a475 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Tue, 20 Feb 2024 14:00:57 +0900 Subject: [PATCH 02/78] =?UTF-8?q?fix(frontend):=20=E3=83=81=E3=83=A3?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=AE=E3=83=A9=E3=83=99=E3=83=AB=E3=81=8C?= =?UTF-8?q?=E6=B6=88=E3=81=88=E3=81=A6=E3=81=84=E3=82=8B=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=20(#13416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): チャートのラベルが消えている問題を修正 * Update CHANGELOG.md --- CHANGELOG.md | 1 + packages/frontend/src/components/MkChart.vue | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb4d1e7ad4..77a03ff684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 - Fix: syuilo/misskeyの時代からあるインスタンスが改変されたバージョンであると誤認識される問題 - Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正 +- Fix: チャートのラベルが消えている問題を修正 ### Server - Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正 diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index dd745c2140..04b6d2f29c 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -240,7 +240,7 @@ const render = () => { }, external: externalTooltipHandler, callbacks: { - label: (item) => chartData?.bytes ? bytes(item.parsed.y * 1000, 1) : item.parsed.y.toString(), + label: (item) => `${item.dataset.label}: ${chartData?.bytes ? bytes(item.parsed.y * 1000, 1) : item.parsed.y.toString()}`, }, }, zoom: props.detailed ? { From f18a31c628aa2ce21599a3706bda06953bfe6e09 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:26:11 +0900 Subject: [PATCH 03/78] =?UTF-8?q?fix(frontend):=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E5=BE=8C=E6=9C=80=E5=88=9D=E3=81=AE=E9=9F=B3?= =?UTF-8?q?=E5=A3=B0=E5=86=8D=E7=94=9F=E3=81=8C=E7=88=86=E9=9F=B3=E3=81=AB?= =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=81=93=E3=81=A8=E3=81=8C=E3=81=82=E3=82=8B?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#13379)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): 画面表示後最初の音声再生が爆音になることがある問題を修正 * Update CHANGELOG.md * Update CHANGELOG.md --- CHANGELOG.md | 1 + packages/frontend/src/scripts/sound.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a03ff684..e1de194da1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Fix: syuilo/misskeyの時代からあるインスタンスが改変されたバージョンであると誤認識される問題 - Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正 - Fix: チャートのラベルが消えている問題を修正 +- Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正 ### Server - Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正 diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 9555579e0d..67818320b3 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -126,7 +126,7 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) */ export function playMisskeySfx(operationType: OperationType) { const sound = defaultStore.state[`sound_${operationType}`]; - if (sound.type == null || !canPlay) return; + if (sound.type == null || !canPlay || !navigator.userActivation.hasBeenActive) return; canPlay = false; playMisskeySfxFile(sound).finally(() => { From bbbb16795d9d09d48ba9efe5a790b28dd4e99cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:27:06 +0900 Subject: [PATCH 04/78] =?UTF-8?q?refactor(frontend):=20=E4=B8=8D=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E3=81=AAconsole.log=E3=82=92=E9=99=A4=E5=8E=BB?= =?UTF-8?q?=E3=83=BB=E6=8A=91=E5=88=B6=20(#13400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(frontend): 不必要なconsole.logを除去 * Update MkCode.core.vue * Update game.board.vue --- packages/frontend/src/components/MkCode.core.vue | 2 +- packages/frontend/src/pages/admin/modlog.vue | 2 -- packages/frontend/src/pages/reversi/game.board.vue | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index f993e983e4..872517b6aa 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -52,7 +52,7 @@ async function fetchLanguage(to: string): Promise { return bundle.id === language || bundle.aliases?.includes(language); }); if (bundles.length > 0) { - console.log(`Loading language: ${language}`); + if (_DEV_) console.log(`Loading language: ${language}`); await highlighter.loadLanguage(bundles[0].import); codeLang.value = language; } else { diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue index 5e251b8a6f..8590ee1651 100644 --- a/packages/frontend/src/pages/admin/modlog.vue +++ b/packages/frontend/src/pages/admin/modlog.vue @@ -54,8 +54,6 @@ const pagination = { })), }; -console.log(Misskey); - const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index cf7cec6b5e..6f7f5b8f38 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -248,7 +248,7 @@ if (game.value.isStarted && !game.value.isEnded) { crc32: crc32.toString(), }).then((res) => { if (res.desynced) { - console.log('resynced'); + if (_DEV_) console.log('resynced'); restoreGame(res.game!); } }); From 750d2626041b355459265a4a4148d08f6ac517fd Mon Sep 17 00:00:00 2001 From: okayurisotto <47853651+okayurisotto@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:31:50 +0900 Subject: [PATCH 05/78] refactor(backend): `ReactionService.prototype.convertLegacyReactions` (#13375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add unit tests * cleanup unnecessary type assertions * `convertedReaction`変数の定義と変換表に対する存在確認処理の整理 * `count`変数の定義とループ処理での`Object.entries()`の活用 * 条件式の整理 * `Array.prototype.reduce`を使うように * `Array.prototype.reduce`を使うように * 配列操作を1つのメソッドチェーンに整理 これまでの実装では、`decodeReaction`の返り値が同一になる異なる入力値が同時に複数個存在した場合、後ろのもので上書きされてしまっていたはず。 これからの実装では、後ろのものは前のものに加算される。 (実際にこの挙動の変更が問題になるシチュエーションはまずないはず。) * add unit test * ドキュメントコメントの追加と型定義の調整 --- packages/backend/src/core/ReactionService.ts | 49 ++++++++++--------- packages/backend/test/unit/ReactionService.ts | 41 ++++++++++++++++ 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 5014156a5c..cb0b079df0 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -322,35 +322,36 @@ export class ReactionService { //#endregion } + /** + * 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、 + * データベース上には存在する「0個のリアクションがついている」という情報を削除する。 + */ @bindThis - public convertLegacyReactions(reactions: Record) { - const _reactions = {} as Record; + public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] { + return Object.entries(reactions) + .filter(([, count]) => { + // `ReactionService.prototype.delete`ではリアクション削除時に、 + // `MiNote['reactions']`のエントリの値をデクリメントしているが、 + // デクリメントしているだけなのでエントリ自体は0を値として持つ形で残り続ける。 + // そのため、この処理がなければ、「0個のリアクションがついている」ということになってしまう。 + return count > 0; + }) + .map(([reaction, count]) => { + // unchecked indexed access + const convertedReaction = legacies[reaction] as string | undefined; - for (const reaction of Object.keys(reactions)) { - if (reactions[reaction] <= 0) continue; + const key = this.decodeReaction(convertedReaction ?? reaction).reaction; - if (Object.keys(legacies).includes(reaction)) { - if (_reactions[legacies[reaction]]) { - _reactions[legacies[reaction]] += reactions[reaction]; - } else { - _reactions[legacies[reaction]] = reactions[reaction]; - } - } else { - if (_reactions[reaction]) { - _reactions[reaction] += reactions[reaction]; - } else { - _reactions[reaction] = reactions[reaction]; - } - } - } + return [key, count] as const; + }) + .reduce((acc, [key, count]) => { + // unchecked indexed access + const prevCount = acc[key] as number | undefined; - const _reactions2 = {} as Record; + acc[key] = (prevCount ?? 0) + count; - for (const reaction of Object.keys(_reactions)) { - _reactions2[this.decodeReaction(reaction).reaction] = _reactions[reaction]; - } - - return _reactions2; + return acc; + }, {}); } @bindThis diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index d1c31cac3a..1957f4544c 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -90,4 +90,45 @@ describe('ReactionService', () => { assert.strictEqual(await reactionService.normalize('unknown'), '❤'); }); }); + + describe('convertLegacyReactions', () => { + test('空の入力に対しては何もしない', () => { + const input = {}; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); + }); + + test('Unicode絵文字リアクションを変換してしまわない', () => { + const input = { '👍': 1, '🍮': 2 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); + }); + + test('カスタム絵文字リアクションを変換してしまわない', () => { + const input = { ':like@.:': 1, ':pudding@example.tld:': 2 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input); + }); + + test('文字列によるレガシーなリアクションを変換する', () => { + const input = { 'like': 1, 'pudding': 2 }; + const output = { '👍': 1, '🍮': 2 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); + }); + + test('host部分が省略されたレガシーなカスタム絵文字リアクションを変換する', () => { + const input = { ':custom_emoji:': 1 }; + const output = { ':custom_emoji@.:': 1 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); + }); + + test('「0個のリアクション」情報を削除する', () => { + const input = { 'angry': 0 }; + const output = {}; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); + }); + + test('host部分の有無によりデコードすると同じ表記になるカスタム絵文字リアクションの個数情報を正しく足し合わせる', () => { + const input = { ':custom_emoji:': 1, ':custom_emoji@.:': 2 }; + const output = { ':custom_emoji@.:': 3 }; + assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output); + }); + }); }); From ae27085f691f331591117f531860b9c510897ae8 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 21 Feb 2024 14:42:37 +0900 Subject: [PATCH 06/78] fix: Bump sharp to 0.33.2 (#13391) --- packages/backend/package.json | 4 +- packages/backend/src/core/FileInfoService.ts | 9 +- pnpm-lock.yaml | 335 +++++++++++++------ 3 files changed, 240 insertions(+), 108 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 86a52faa05..3a3d8e0411 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/sharp-read-bmp": "^1.1.1", + "@misskey-dev/sharp-read-bmp": "^1.2.0", "@misskey-dev/summaly": "^5.0.3", "@nestjs/common": "10.2.10", "@nestjs/core": "10.2.10", @@ -164,7 +164,7 @@ "rxjs": "7.8.1", "sanitize-html": "2.11.0", "secure-json-parse": "2.7.0", - "sharp": "0.32.6", + "sharp": "0.33.2", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index b177367a16..b8babcb3a7 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -15,6 +15,7 @@ import isSvg from 'is-svg'; import probeImageSize from 'probe-image-size'; import { type predictionType } from 'nsfwjs'; import sharp from 'sharp'; +import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import { encode } from 'blurhash'; import { createTempDir } from '@/misc/create-temp.js'; import { AiService } from '@/core/AiService.js'; @@ -122,7 +123,7 @@ export class FileInfoService { 'image/avif', 'image/svg+xml', ].includes(type.mime)) { - blurhash = await this.getBlurhash(path).catch(e => { + blurhash = await this.getBlurhash(path, type.mime).catch(e => { warnings.push(`getBlurhash failed: ${e}`); return undefined; }); @@ -407,9 +408,9 @@ export class FileInfoService { * Calculate average color of image */ @bindThis - private getBlurhash(path: string): Promise { - return new Promise((resolve, reject) => { - sharp(path) + private getBlurhash(path: string, type: string): Promise { + return new Promise(async (resolve, reject) => { + (await sharpBmp(path, type)) .raw() .ensureAlpha() .resize(64, 64, { fit: 'inside' }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f89f8a417d..d7b2fb1f2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/sharp-read-bmp': - specifier: ^1.1.1 - version: 1.1.1 + specifier: ^1.2.0 + version: 1.2.0 '@misskey-dev/summaly': specifier: ^5.0.3 version: 5.0.3 @@ -366,8 +366,8 @@ importers: specifier: 2.7.0 version: 2.7.0 sharp: - specifier: 0.32.6 - version: 0.32.6 + specifier: 0.33.2 + version: 0.33.2 slacc: specifier: 0.0.10 version: 0.0.10 @@ -3370,6 +3370,14 @@ packages: engines: {node: '>=10.0.0'} dev: true + /@emnapi/runtime@0.45.0: + resolution: {integrity: sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==} + requiresBuild: true + dependencies: + tslib: 2.6.2 + dev: false + optional: true + /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} peerDependencies: @@ -4017,6 +4025,194 @@ packages: resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} dev: true + /@img/sharp-darwin-arm64@0.33.2: + resolution: {integrity: sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.1 + dev: false + optional: true + + /@img/sharp-darwin-x64@0.33.2: + resolution: {integrity: sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.1 + dev: false + optional: true + + /@img/sharp-libvips-darwin-arm64@1.0.1: + resolution: {integrity: sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==} + engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-darwin-x64@1.0.1: + resolution: {integrity: sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==} + engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-arm64@1.0.1: + resolution: {integrity: sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-arm@1.0.1: + resolution: {integrity: sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-s390x@1.0.1: + resolution: {integrity: sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-x64@1.0.1: + resolution: {integrity: sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linuxmusl-arm64@1.0.1: + resolution: {integrity: sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linuxmusl-x64@1.0.1: + resolution: {integrity: sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-linux-arm64@0.33.2: + resolution: {integrity: sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.1 + dev: false + optional: true + + /@img/sharp-linux-arm@0.33.2: + resolution: {integrity: sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==} + engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.1 + dev: false + optional: true + + /@img/sharp-linux-s390x@0.33.2: + resolution: {integrity: sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==} + engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.1 + dev: false + optional: true + + /@img/sharp-linux-x64@0.33.2: + resolution: {integrity: sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.1 + dev: false + optional: true + + /@img/sharp-linuxmusl-arm64@0.33.2: + resolution: {integrity: sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.1 + dev: false + optional: true + + /@img/sharp-linuxmusl-x64@0.33.2: + resolution: {integrity: sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.1 + dev: false + optional: true + + /@img/sharp-wasm32@0.33.2: + resolution: {integrity: sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [wasm32] + requiresBuild: true + dependencies: + '@emnapi/runtime': 0.45.0 + dev: false + optional: true + + /@img/sharp-win32-ia32@0.33.2: + resolution: {integrity: sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-win32-x64@0.33.2: + resolution: {integrity: sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@ioredis/commands@1.2.0: resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} dev: false @@ -4464,12 +4660,12 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0) dev: true - /@misskey-dev/sharp-read-bmp@1.1.1: - resolution: {integrity: sha512-X52BQYL/I9mafypQ+wBhst+BUlYiPWnHhKGcF6ybcYSLl+zhcV0q5mezIXHozhM0Sv0A7xCdrWmR7TCNxHLrtQ==} + /@misskey-dev/sharp-read-bmp@1.2.0: + resolution: {integrity: sha512-er4pRakXzHYfEgOFAFfQagqDouG+wLm+kwNq1I30oSdIHDa0wM3KjFpfIGQ25Fks4GcmOl1s7Zh6xoQu5dNjTw==} dependencies: decode-bmp: 0.2.1 decode-ico: 0.4.1 - sharp: 0.32.6 + sharp: 0.33.2 dev: false /@misskey-dev/summaly@5.0.3: @@ -5076,10 +5272,6 @@ packages: /@simplewebauthn/types@9.0.1: resolution: {integrity: sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==} - /@sinclair/typebox@0.24.51: - resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} - dev: true - /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -8833,6 +9025,7 @@ packages: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.0 + dev: true /blob-util@2.0.2: resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} @@ -8981,6 +9174,7 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + dev: true /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -10197,11 +10391,6 @@ packages: which-typed-array: 1.1.11 dev: true - /deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - dev: false - /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -11193,11 +11382,6 @@ packages: engines: {node: '>= 0.8.0'} dev: true - /expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - dev: false - /expect@29.7.0: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -11688,6 +11872,7 @@ packages: /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: true /fs-extra@11.1.1: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} @@ -11905,10 +12090,6 @@ packages: - supports-color dev: true - /github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - dev: false - /github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} dev: true @@ -12481,6 +12662,7 @@ packages: /ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true /ini@2.0.0: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} @@ -14697,6 +14879,7 @@ packages: /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: true /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} @@ -14870,10 +15053,6 @@ packages: hasBin: true dev: false - /napi-build-utils@1.0.2: - resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} - dev: false - /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -14948,21 +15127,10 @@ packages: path-to-regexp: 1.8.0 dev: true - /node-abi@3.31.0: - resolution: {integrity: sha512-eSKV6s+APenqVh8ubJyiu/YhZgxQpGP66ntzUb3lY1xB9ukSRaGnx0AIxI+IM+1+IVYC1oWobgG5L3Lt9ARykQ==} - engines: {node: '>=10'} - dependencies: - semver: 7.5.4 - dev: false - /node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} dev: false - /node-addon-api@6.1.0: - resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} - dev: false - /node-bitmap@0.0.1: resolution: {integrity: sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA==} engines: {node: '>=v0.6.5'} @@ -16237,25 +16405,6 @@ packages: resolution: {integrity: sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==} dev: true - /prebuild-install@7.1.1: - resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} - engines: {node: '>=10'} - hasBin: true - dependencies: - detect-libc: 2.0.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 1.0.2 - node-abi: 3.31.0 - pump: 3.0.0 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.1 - tunnel-agent: 0.6.0 - dev: false - /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -16655,16 +16804,6 @@ packages: iconv-lite: 0.4.24 unpipe: 1.0.0 - /rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - dev: false - /rdf-canonize@3.4.0: resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==} engines: {node: '>=12'} @@ -17395,19 +17534,34 @@ packages: kind-of: 6.0.3 dev: true - /sharp@0.32.6: - resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} - engines: {node: '>=14.15.0'} + /sharp@0.33.2: + resolution: {integrity: sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==} + engines: {libvips: '>=8.15.1', node: ^18.17.0 || ^20.3.0 || >=21.0.0} requiresBuild: true dependencies: color: 4.2.3 detect-libc: 2.0.2 - node-addon-api: 6.1.0 - prebuild-install: 7.1.1 semver: 7.5.4 - simple-get: 4.0.1 - tar-fs: 3.0.4 - tunnel-agent: 0.6.0 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.2 + '@img/sharp-darwin-x64': 0.33.2 + '@img/sharp-libvips-darwin-arm64': 1.0.1 + '@img/sharp-libvips-darwin-x64': 1.0.1 + '@img/sharp-libvips-linux-arm': 1.0.1 + '@img/sharp-libvips-linux-arm64': 1.0.1 + '@img/sharp-libvips-linux-s390x': 1.0.1 + '@img/sharp-libvips-linux-x64': 1.0.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.1 + '@img/sharp-libvips-linuxmusl-x64': 1.0.1 + '@img/sharp-linux-arm': 0.33.2 + '@img/sharp-linux-arm64': 0.33.2 + '@img/sharp-linux-s390x': 0.33.2 + '@img/sharp-linux-x64': 0.33.2 + '@img/sharp-linuxmusl-arm64': 0.33.2 + '@img/sharp-linuxmusl-x64': 0.33.2 + '@img/sharp-wasm32': 0.33.2 + '@img/sharp-win32-ia32': 0.33.2 + '@img/sharp-win32-x64': 0.33.2 dev: false /shebang-command@1.2.0: @@ -17456,18 +17610,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - /simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - dev: false - - /simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - dev: false - /simple-oauth2@5.0.0: resolution: {integrity: sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==} dependencies: @@ -18042,11 +18184,6 @@ packages: min-indent: 1.0.1 dev: true - /strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - dev: false - /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -18154,14 +18291,7 @@ packages: mkdirp-classic: 0.5.3 pump: 3.0.0 tar-stream: 2.2.0 - - /tar-fs@3.0.4: - resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} - dependencies: - mkdirp-classic: 0.5.3 - pump: 3.0.0 - tar-stream: 3.1.6 - dev: false + dev: true /tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} @@ -18172,6 +18302,7 @@ packages: fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.0 + dev: true /tar-stream@3.1.6: resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} From fb0eb5a31fb44cddc5d517fbd5b65b670bc60e4f Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:35:05 +0900 Subject: [PATCH 07/78] :art: --- packages/frontend/src/ui/deck/channel-column.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 125c85130e..bd3b059497 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only From e10ce7204cf54408d533f5f716f4d6a98aed86ed Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Wed, 21 Feb 2024 20:15:04 +0900 Subject: [PATCH 08/78] =?UTF-8?q?fix:=20MkUserPopup=E3=81=8C=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=81=95=E3=82=8C=E3=81=A6=E3=82=8B=E7=8A=B6=E6=85=8B?= =?UTF-8?q?=E3=81=A7v-user-preview=E3=81=8C=E3=81=A4=E3=81=84=E3=81=9F?= =?UTF-8?q?=E8=A6=81=E7=B4=A0=E3=81=8Cdetach=E3=81=95=E3=82=8C=E3=82=8B?= =?UTF-8?q?=E3=81=A8MkUserPopup=E3=81=8C=E6=B6=88=E3=81=88=E3=81=AA?= =?UTF-8?q?=E3=81=84=E5=95=8F=E9=A1=8C=20(#13349)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: MkUserPopupが表示されてる状態でv-user-previewがついた要素がdetachされるとMkUserPopupが消えない問題 * docs(changelog): previewの中のユーザメンションをホバーした状態で投稿を編集するとユーザの情報popupが消えない問題を修正 * docs(changelog): ユーザの情報のポップアップが消えなくなることがある問題を修正 --- CHANGELOG.md | 1 + packages/frontend/src/directives/user-preview.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1de194da1..027f05c922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ - Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正 - Fix: MkCodeEditorで行がずれていってしまう問題の修正 - Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196 +- Fix: ユーザの情報のポップアップが消えなくなることがある問題を修正 ### Server - Enhance: 連合先のレートリミットを超過した際にリトライするようになりました diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts index 0d6c330da1..7a008a4486 100644 --- a/packages/frontend/src/directives/user-preview.ts +++ b/packages/frontend/src/directives/user-preview.ts @@ -99,7 +99,6 @@ export class UserPreview { this.el.removeEventListener('mouseover', this.onMouseover); this.el.removeEventListener('mouseleave', this.onMouseleave); this.el.removeEventListener('click', this.onClick); - window.clearInterval(this.checkTimer); } } From b36e6b1a777848ec8553b297e956ada240dcacc9 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 22 Feb 2024 00:59:59 +0900 Subject: [PATCH 09/78] =?UTF-8?q?fix:=20=E7=A6=81=E6=AD=A2=E3=82=AD?= =?UTF-8?q?=E3=83=BC=E3=83=AF=E3=83=BC=E3=83=89=E3=82=92=E5=90=AB=E3=82=80?= =?UTF-8?q?=E3=83=8E=E3=83=BC=E3=83=88=E3=81=8CDelayed=20Queue=E3=81=AB?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=95=E3=82=8C=E3=81=A6=E5=86=8D=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=81=95=E3=82=8C=E3=82=8B=E5=95=8F=E9=A1=8C=20(#1342?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: use IdentifiableError instead of NoteCreateService.ContainsProhibitedWordsError * fix: notes with prohibited words are reprocessed with delay * docs(changelog): 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題 * lint: fix lint errors * fix: rethrowするべきなのにrethrowし忘れていたのを修正 --- CHANGELOG.md | 1 + packages/backend/src/core/NoteCreateService.ts | 5 ++--- .../src/queue/processors/InboxProcessorService.ts | 10 +++++++++- .../backend/src/server/api/endpoints/notes/create.ts | 5 +++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 027f05c922..63f5c913f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ ### Server - Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正 +- Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正 ## 2024.2.0 diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 9cec614d5c..2a5fd2e1a6 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -59,6 +59,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -151,8 +152,6 @@ type Option = { export class NoteCreateService implements OnApplicationShutdown { #shutdownController = new AbortController(); - public static ContainsProhibitedWordsError = class extends Error {}; - constructor( @Inject(DI.config) private config: Config, @@ -264,7 +263,7 @@ export class NoteCreateService implements OnApplicationShutdown { } if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) { - throw new NoteCreateService.ContainsProhibitedWordsError(); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host); diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 7adadd799b..0a713149e5 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -24,6 +24,7 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { InboxJobData } from '../types.js'; @@ -180,7 +181,14 @@ export class InboxProcessorService { }); // アクティビティを処理 - await this.apInboxService.performActivity(authUser.user, activity); + try { + await this.apInboxService.performActivity(authUser.user, activity); + } catch (e) { + if (e instanceof IdentifiableError) { + if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') return 'blocked notes with prohibited words'; + } + throw e; + } return 'ok'; } } diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index e6e4fcc745..2fa0bd099f 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -19,6 +19,7 @@ import { DI } from '@/di-symbols.js'; import { isPureRenote } from '@/misc/is-pure-renote.js'; import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -376,8 +377,8 @@ export default class extends Endpoint { // eslint- }; } catch (e) { // TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい - if (e instanceof NoteCreateService.ContainsProhibitedWordsError) { - throw new ApiError(meta.errors.containsProhibitedWords); + if (e instanceof IdentifiableError) { + if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords); } throw e; From 26c8b53f701df76a42897af18f0a117a30226662 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:59:52 +0900 Subject: [PATCH 10/78] =?UTF-8?q?enhance:=20=E3=82=B5=E3=83=BC=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=81=94=E3=81=A8=E3=81=AB=E3=83=A2=E3=83=87=E3=83=AC?= =?UTF-8?q?=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=8E=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=82=92=E6=AE=8B=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/index.d.ts | 6 +++++- locales/ja-JP.yml | 3 ++- .../1708399372194-per-instance-mod-note.js | 16 ++++++++++++++++ .../src/core/entities/InstanceEntityService.ts | 9 ++++++++- packages/backend/src/models/Instance.ts | 5 +++++ .../models/json-schema/federation-instance.ts | 4 ++++ .../admin/federation/update-instance.ts | 15 +++++++++++++-- .../api/endpoints/federation/show-instance.ts | 2 +- packages/backend/src/types.ts | 7 +++++++ .../frontend/src/pages/admin/modlog.ModLog.vue | 6 ++++++ packages/frontend/src/pages/instance-info.vue | 12 +++++++++++- packages/misskey-js/etc/misskey-js.api.md | 5 ++++- packages/misskey-js/src/autogen/types.ts | 4 +++- packages/misskey-js/src/consts.ts | 7 +++++++ packages/misskey-js/src/entities.ts | 3 +++ 16 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 packages/backend/migration/1708399372194-per-instance-mod-note.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e1de194da1..31850e1d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ## 202x.x.x (unreleased) ### General +- Enhance: サーバーごとにモデレーションノートを残せるように ### Client - Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 diff --git a/locales/index.d.ts b/locales/index.d.ts index 1bc99ab849..d483fea837 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9172,7 +9172,7 @@ export interface Locale extends ILocale { */ "updateServerSettings": string; /** - * モデレーションノート更新 + * ユーザーのモデレーションノート更新 */ "updateUserNote": string; /** @@ -9219,6 +9219,10 @@ export interface Locale extends ILocale { * リモートサーバーを再開 */ "unsuspendRemoteInstance": string; + /** + * リモートサーバーのモデレーションノート更新 + */ + "updateRemoteInstanceNote": string; /** * ファイルをセンシティブ付与 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5993ec80d0..7e16619fc7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2434,7 +2434,7 @@ _moderationLogTypes: updateCustomEmoji: "カスタム絵文字更新" deleteCustomEmoji: "カスタム絵文字削除" updateServerSettings: "サーバー設定更新" - updateUserNote: "モデレーションノート更新" + updateUserNote: "ユーザーのモデレーションノート更新" deleteDriveFile: "ファイルを削除" deleteNote: "ノートを削除" createGlobalAnnouncement: "全体のお知らせを作成" @@ -2446,6 +2446,7 @@ _moderationLogTypes: resetPassword: "パスワードをリセット" suspendRemoteInstance: "リモートサーバーを停止" unsuspendRemoteInstance: "リモートサーバーを再開" + updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新" markSensitiveDriveFile: "ファイルをセンシティブ付与" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" resolveAbuseReport: "通報を解決" diff --git a/packages/backend/migration/1708399372194-per-instance-mod-note.js b/packages/backend/migration/1708399372194-per-instance-mod-note.js new file mode 100644 index 0000000000..339a4d7af9 --- /dev/null +++ b/packages/backend/migration/1708399372194-per-instance-mod-note.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class PerInstanceModNote1708399372194 { + name = 'PerInstanceModNote1708399372194' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" ADD "moderationNote" character varying(16384) NOT NULL DEFAULT ''`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "moderationNote"`); + } +} diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 9287c98003..e46bd8b963 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -8,12 +8,15 @@ import type { Packed } from '@/misc/json-schema.js'; import type { MiInstance } from '@/models/Instance.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; -import { UtilityService } from '../UtilityService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { MiUser } from '@/models/User.js'; @Injectable() export class InstanceEntityService { constructor( private metaService: MetaService, + private roleService: RoleService, private utilityService: UtilityService, ) { @@ -22,8 +25,11 @@ export class InstanceEntityService { @bindThis public async pack( instance: MiInstance, + me?: { id: MiUser['id']; } | null | undefined, ): Promise> { const meta = await this.metaService.fetch(); + const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; + return { id: instance.id, firstRetrievedAt: instance.firstRetrievedAt.toISOString(), @@ -48,6 +54,7 @@ export class InstanceEntityService { themeColor: instance.themeColor, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, + moderationNote: iAmModerator ? instance.moderationNote : null, }; } diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts index 0632ef525b..9863c9d75d 100644 --- a/packages/backend/src/models/Instance.ts +++ b/packages/backend/src/models/Instance.ts @@ -144,4 +144,9 @@ export class MiInstance { nullable: true, }) public infoUpdatedAt: Date | null; + + @Column('varchar', { + length: 16384, default: '', + }) + public moderationNote: string; } diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 087a0e6967..42d98fe523 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -107,5 +107,9 @@ export const packedFederationInstanceSchema = { optional: false, nullable: true, format: 'date-time', }, + moderationNote: { + type: 'string', + optional: true, nullable: true, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index b989b99e47..0bcdc2a4b8 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -24,8 +24,9 @@ export const paramDef = { properties: { host: { type: 'string' }, isSuspended: { type: 'boolean' }, + moderationNote: { type: 'string' }, }, - required: ['host', 'isSuspended'], + required: ['host'], } as const; @Injectable() @@ -47,9 +48,10 @@ export default class extends Endpoint { // eslint- await this.federatedInstanceService.update(instance.id, { isSuspended: ps.isSuspended, + moderationNote: ps.moderationNote, }); - if (instance.isSuspended !== ps.isSuspended) { + if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) { if (ps.isSuspended) { this.moderationLogService.log(me, 'suspendRemoteInstance', { id: instance.id, @@ -62,6 +64,15 @@ export default class extends Endpoint { // eslint- }); } } + + if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) { + this.moderationLogService.log(me, 'updateRemoteInstanceNote', { + id: instance.id, + host: instance.host, + before: instance.moderationNote, + after: ps.moderationNote, + }); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index e3c598d110..2972861a4b 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -43,7 +43,7 @@ export default class extends Endpoint { // eslint- const instance = await this.instancesRepository .findOneBy({ host: this.utilityService.toPuny(ps.host) }); - return instance ? await this.instanceEntityService.pack(instance) : null; + return instance ? await this.instanceEntityService.pack(instance, me) : null; }); } } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index fdcd2c0629..506a755cc4 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -69,6 +69,7 @@ export const moderationLogTypes = [ 'resetPassword', 'suspendRemoteInstance', 'unsuspendRemoteInstance', + 'updateRemoteInstanceNote', 'markSensitiveDriveFile', 'unmarkSensitiveDriveFile', 'resolveAbuseReport', @@ -209,6 +210,12 @@ export type ModerationLogPayloads = { id: string; host: string; }; + updateRemoteInstanceNote: { + id: string; + host: string; + before: string | null; + after: string | null; + }; markSensitiveDriveFile: { fileId: string; fileUserId: string | null; diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 21d68331cb..e33c882721 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -110,6 +110,12 @@ SPDX-License-Identifier: AGPL-3.0-only +
raw diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 2f1557182a..cb7fe2866c 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -39,6 +39,9 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.blockThisInstance }} {{ i18n.ts.silenceThisInstance }} Refresh metadata + + + @@ -119,7 +122,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index 2b559f92c9..fe1b7c561d 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -169,7 +169,7 @@ function save() { feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value, manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)), }).then(() => { - fetchInstance(); + fetchInstance(true); }); } diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue index 839b9bee16..4a858887f3 100644 --- a/packages/frontend/src/pages/admin/email-settings.vue +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -124,7 +124,7 @@ function save() { smtpUser: smtpUser.value, smtpPass: smtpPass.value, }).then(() => { - fetchInstance(); + fetchInstance(true); }); } diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index ba3eb05e72..e0b82eb02e 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -61,7 +61,7 @@ function save() { deeplAuthKey: deeplAuthKey.value, deeplIsPro: deeplIsPro.value, }).then(() => { - fetchInstance(); + fetchInstance(true); }); } diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue index 5167b2e6b2..6b14bd42c2 100644 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -50,7 +50,7 @@ function save() { silencedHosts: silencedHosts.value.split('\n') || [], }).then(() => { - fetchInstance(); + fetchInstance(true); }); } diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index d6cb1e39a7..9efb34ac9a 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -110,7 +110,7 @@ function save() { hiddenTags: hiddenTags.value.split('\n'), preservedUsernames: preservedUsernames.value.split('\n'), }).then(() => { - fetchInstance(); + fetchInstance(true); }); } diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue index 4ff5ab09ca..5fddb715cd 100644 --- a/packages/frontend/src/pages/admin/object-storage.vue +++ b/packages/frontend/src/pages/admin/object-storage.vue @@ -143,7 +143,7 @@ function save() { objectStorageSetPublicRead: objectStorageSetPublicRead.value, objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value, }).then(() => { - fetchInstance(); + fetchInstance(true); }); } diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue index 651f0ef936..345cf333b5 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/other-settings.vue @@ -73,7 +73,7 @@ function save() { enableChartsForRemoteUser: enableChartsForRemoteUser.value, enableChartsForFederatedInstances: enableChartsForFederatedInstances.value, }).then(() => { - fetchInstance(); + fetchInstance(true); }); } diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue index 02b506d13d..81db9f1da9 100644 --- a/packages/frontend/src/pages/admin/proxy-account.vue +++ b/packages/frontend/src/pages/admin/proxy-account.vue @@ -56,7 +56,7 @@ function save() { os.apiWithDialog('admin/update-meta', { proxyAccountId: proxyAccountId.value, }).then(() => { - fetchInstance(); + fetchInstance(true); }); } diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index cadcf5a8cc..c4745978df 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -196,7 +196,7 @@ async function init() { enableTruemailApi.value = meta.enableTruemailApi; truemailInstance.value = meta.truemailInstance; truemailAuthKey.value = meta.truemailAuthKey; - bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || ""; + bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || ''; } function save() { @@ -221,7 +221,7 @@ function save() { truemailAuthKey: truemailAuthKey.value, bannedEmailDomains: bannedEmailDomains.value.split('\n'), }).then(() => { - fetchInstance(); + fetchInstance(true); }); } diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue index 87318bccce..ff9b8d6299 100644 --- a/packages/frontend/src/pages/admin/server-rules.vue +++ b/packages/frontend/src/pages/admin/server-rules.vue @@ -58,7 +58,7 @@ const save = async () => { await os.apiWithDialog('admin/update-meta', { serverRules: serverRules.value, }); - fetchInstance(); + fetchInstance(true); }; const remove = (index: number): void => { diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index c505d70aa9..9a198ee8a3 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -243,7 +243,7 @@ async function save(): void { notesPerOneAd: notesPerOneAd.value, }); - fetchInstance(); + fetchInstance(true); } const headerTabs = computed(() => []); diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts index f2db87c4fb..b20109ec72 100644 --- a/packages/frontend/src/scripts/clear-cache.ts +++ b/packages/frontend/src/scripts/clear-cache.ts @@ -2,14 +2,18 @@ import { unisonReload } from '@/scripts/unison-reload.js'; import * as os from '@/os.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; +import { fetchInstance } from '@/instance.js'; export async function clearCache() { os.waiting(); + miLocalStorage.removeItem('instance'); + miLocalStorage.removeItem('instanceCachedAt'); miLocalStorage.removeItem('locale'); miLocalStorage.removeItem('localeVersion'); miLocalStorage.removeItem('theme'); miLocalStorage.removeItem('emojis'); miLocalStorage.removeItem('lastEmojisFetchedAt'); + await fetchInstance(true); await fetchCustomEmojis(true); unisonReload(); } diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index c2428910fa..a2d5a4f514 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1715,7 +1715,10 @@ declare namespace entities { Role, RolePolicies, ReversiGameLite, - ReversiGameDetailed + ReversiGameDetailed, + MetaLite, + MetaDetailedOnly, + MetaDetailed } } export { entities } @@ -2223,6 +2226,15 @@ type MeDetailed = components['schemas']['MeDetailed']; // @public (undocumented) type MeDetailedOnly = components['schemas']['MeDetailedOnly']; +// @public (undocumented) +type MetaDetailed = components['schemas']['MetaDetailed']; + +// @public (undocumented) +type MetaDetailedOnly = components['schemas']['MetaDetailedOnly']; + +// @public (undocumented) +type MetaLite = components['schemas']['MetaLite']; + // @public (undocumented) type MetaRequest = operations['meta']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 6400567a2d..ab49f9478a 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -46,3 +46,6 @@ export type Role = components['schemas']['Role']; export type RolePolicies = components['schemas']['RolePolicies']; export type ReversiGameLite = components['schemas']['ReversiGameLite']; export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; +export type MetaLite = components['schemas']['MetaLite']; +export type MetaDetailedOnly = components['schemas']['MetaDetailedOnly']; +export type MetaDetailed = components['schemas']['MetaDetailed']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 0b2a88b537..18bc45b983 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4724,6 +4724,96 @@ export type components = { logs: number[][]; map: string[]; }; + MetaLite: { + maintainerName: string | null; + maintainerEmail: string | null; + version: string; + providesTarball: boolean; + name: string | null; + shortName: string | null; + /** + * Format: url + * @example https://misskey.example.com + */ + uri: string; + description: string | null; + langs: string[]; + tosUrl: string | null; + /** @default https://github.com/misskey-dev/misskey */ + repositoryUrl: string | null; + /** @default https://github.com/misskey-dev/misskey/issues/new */ + feedbackUrl: string | null; + defaultDarkTheme: string | null; + defaultLightTheme: string | null; + disableRegistration: boolean; + emailRequiredForSignup: boolean; + enableHcaptcha: boolean; + hcaptchaSiteKey: string | null; + enableMcaptcha: boolean; + mcaptchaSiteKey: string | null; + mcaptchaInstanceUrl: string | null; + enableRecaptcha: boolean; + recaptchaSiteKey: string | null; + enableTurnstile: boolean; + turnstileSiteKey: string | null; + swPublickey: string | null; + /** @default /assets/ai.png */ + mascotImageUrl: string; + bannerUrl: string | null; + serverErrorImageUrl: string | null; + infoImageUrl: string | null; + notFoundImageUrl: string | null; + iconUrl: string | null; + maxNoteTextLength: number; + ads: { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: url */ + url: string; + place: string; + ratio: number; + /** Format: url */ + imageUrl: string; + dayOfWeek: number; + }[]; + /** @default 0 */ + notesPerOneAd: number; + enableEmail: boolean; + enableServiceWorker: boolean; + translatorAvailable: boolean; + mediaProxy: string; + backgroundImageUrl: string | null; + impressumUrl: string | null; + logoImageUrl: string | null; + privacyPolicyUrl: string | null; + serverRules: string[]; + themeColor: string | null; + policies: components['schemas']['RolePolicies']; + }; + MetaDetailedOnly: { + features?: { + registration: boolean; + emailRequiredForSignup: boolean; + localTimeline: boolean; + globalTimeline: boolean; + hcaptcha: boolean; + turnstile: boolean; + recaptcha: boolean; + objectStorage: boolean; + serviceWorker: boolean; + /** @default true */ + miauth?: boolean; + }; + proxyAccountName: string | null; + /** @example false */ + requireSetup: boolean; + cacheRemoteFiles: boolean; + cacheRemoteSensitiveFiles: boolean; + }; + MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly']; }; responses: never; parameters: never; @@ -19448,91 +19538,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { - maintainerName: string | null; - maintainerEmail: string | null; - version: string; - providesTarball: boolean; - name: string; - shortName: string | null; - /** - * Format: url - * @example https://misskey.example.com - */ - uri: string; - description: string | null; - langs: string[]; - tosUrl: string | null; - /** @default https://github.com/misskey-dev/misskey */ - repositoryUrl: string | null; - /** @default https://github.com/misskey-dev/misskey/issues/new */ - feedbackUrl: string | null; - defaultDarkTheme: string | null; - defaultLightTheme: string | null; - disableRegistration: boolean; - cacheRemoteFiles: boolean; - cacheRemoteSensitiveFiles: boolean; - emailRequiredForSignup: boolean; - enableHcaptcha: boolean; - hcaptchaSiteKey: string | null; - enableMcaptcha: boolean; - mcaptchaSiteKey: string | null; - mcaptchaInstanceUrl: string | null; - enableRecaptcha: boolean; - recaptchaSiteKey: string | null; - enableTurnstile: boolean; - turnstileSiteKey: string | null; - swPublickey: string | null; - /** @default /assets/ai.png */ - mascotImageUrl: string; - bannerUrl: string; - serverErrorImageUrl: string | null; - infoImageUrl: string | null; - notFoundImageUrl: string | null; - iconUrl: string | null; - maxNoteTextLength: number; - ads: { - /** - * Format: id - * @example xxxxxxxxxx - */ - id: string; - /** Format: url */ - url: string; - place: string; - ratio: number; - /** Format: url */ - imageUrl: string; - dayOfWeek: number; - }[]; - /** @default 0 */ - notesPerOneAd: number; - /** @example false */ - requireSetup: boolean; - enableEmail: boolean; - enableServiceWorker: boolean; - translatorAvailable: boolean; - proxyAccountName: string | null; - mediaProxy: string; - features?: { - registration: boolean; - localTimeline: boolean; - globalTimeline: boolean; - hcaptcha: boolean; - recaptcha: boolean; - objectStorage: boolean; - serviceWorker: boolean; - /** @default true */ - miauth?: boolean; - }; - backgroundImageUrl: string | null; - impressumUrl: string | null; - logoImageUrl: string | null; - privacyPolicyUrl: string | null; - serverRules: string[]; - themeColor: string | null; - policies: components['schemas']['RolePolicies']; - }; + 'application/json': components['schemas']['MetaLite'] | components['schemas']['MetaDetailed']; }; }; /** @description Client error */ From 080a3c20bd7f7d6ca7a30fa5a94d8431a6a9c688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:10:13 +0900 Subject: [PATCH 14/78] =?UTF-8?q?fix:=20SSR=E6=99=82=E3=81=AEmeta=E3=82=92?= =?UTF-8?q?=E3=82=A8=E3=82=B9=E3=82=B1=E3=83=BC=E3=83=97=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=20(#13440)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: SSR時のmetaをエスケープするように * エスケープ方法を変更 --- packages/backend/package.json | 2 ++ .../backend/src/server/web/ClientServerService.ts | 4 ++-- pnpm-lock.yaml | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 3a3d8e0411..1745277b41 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -118,6 +118,7 @@ "got": "14.1.0", "happy-dom": "10.0.3", "hpagent": "1.2.0", + "htmlescape": "^1.1.1", "http-link-header": "1.1.1", "ioredis": "5.3.2", "ip-cidr": "3.1.0", @@ -194,6 +195,7 @@ "@types/color-convert": "2.0.3", "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.24", + "@types/htmlescape": "^1.1.3", "@types/http-link-header": "1.0.5", "@types/jest": "29.5.11", "@types/js-yaml": "4.0.9", diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index e8908f50ec..b1af0c3df6 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -19,6 +19,7 @@ import fastifyView from '@fastify/view'; import fastifyCookie from '@fastify/cookie'; import fastifyProxy from '@fastify/http-proxy'; import vary from 'vary'; +import htmlSafeJsonStringify from 'htmlescape'; import type { Config } from '@/config.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; import { DI } from '@/di-symbols.js'; @@ -34,7 +35,6 @@ import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; -import { deepClone } from '@/misc/clone.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; @@ -185,7 +185,7 @@ export class ClientServerService { infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', instanceUrl: this.config.url, - metaJson: JSON.stringify(await this.metaEntityService.packDetailed(meta)), + metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)), now: Date.now(), }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7b2fb1f2f..ca86ad0445 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,6 +227,9 @@ importers: hpagent: specifier: 1.2.0 version: 1.2.0 + htmlescape: + specifier: ^1.1.1 + version: 1.1.1 http-link-header: specifier: 1.1.1 version: 1.1.1 @@ -538,6 +541,9 @@ importers: '@types/fluent-ffmpeg': specifier: 2.1.24 version: 2.1.24 + '@types/htmlescape': + specifier: ^1.1.3 + version: 1.1.3 '@types/http-link-header': specifier: 1.0.5 version: 1.0.5 @@ -7405,6 +7411,10 @@ packages: '@types/unist': 3.0.2 dev: true + /@types/htmlescape@1.1.3: + resolution: {integrity: sha512-tuC81YJXGUe0q8WRtBNW+uyx79rkkzWK651ALIXXYq5/u/IxjX4iHneGF2uUqzsNp+F+9J2mFZOv9jiLTtIq0w==} + dev: true + /@types/http-cache-semantics@4.0.4: resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -12456,6 +12466,11 @@ packages: engines: {node: '>=8'} dev: true + /htmlescape@1.1.1: + resolution: {integrity: sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==} + engines: {node: '>=0.10'} + dev: false + /htmlparser2@8.0.1: resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==} dependencies: From 64953fadc92abf3fd83186733282bcf4f39bdb49 Mon Sep 17 00:00:00 2001 From: okayurisotto <47853651+okayurisotto@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:12:57 +0900 Subject: [PATCH 15/78] =?UTF-8?q?refactor(backend):=20`Array.prototype.fil?= =?UTF-8?q?ter`=E3=81=A7=E3=81=AE=E9=9D=9Enull=E7=A2=BA=E8=AA=8D=E3=81=A7?= =?UTF-8?q?=E3=81=AF`isNotNull`=E9=96=A2=E6=95=B0=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E3=81=86=E3=82=88=E3=81=86=E3=81=AB=20(#13442)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `Array.prototype.filter`での非null確認では`isNotNull`関数を使うように * `{}` -> `NonNullable` --- packages/backend/src/core/NoteCreateService.ts | 3 ++- packages/backend/src/core/activitypub/ApAudienceService.ts | 3 ++- packages/backend/src/core/activitypub/ApInboxService.ts | 3 ++- packages/backend/src/core/activitypub/ApRendererService.ts | 2 +- .../backend/src/core/activitypub/models/ApMentionService.ts | 3 ++- packages/backend/src/core/activitypub/models/ApNoteService.ts | 3 ++- .../backend/src/core/activitypub/models/ApPersonService.ts | 3 ++- .../backend/src/core/activitypub/models/ApQuestionService.ts | 3 ++- packages/backend/src/core/activitypub/models/tag.ts | 3 ++- packages/backend/src/core/entities/DriveFileEntityService.ts | 2 +- packages/backend/src/core/entities/PageEntityService.ts | 3 ++- packages/backend/src/core/entities/UserEntityService.ts | 3 ++- packages/backend/src/misc/is-not-null.ts | 4 +--- .../backend/src/server/api/endpoints/gallery/posts/create.ts | 3 ++- .../backend/src/server/api/endpoints/gallery/posts/update.ts | 3 ++- packages/backend/src/server/api/endpoints/pinned-users.ts | 3 ++- 16 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 2a5fd2e1a6..b412d5db11 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -59,6 +59,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -816,7 +817,7 @@ export class NoteCreateService implements OnApplicationShutdown { const mentions = extractMentions(tokens); let mentionedUsers = (await Promise.all(mentions.map(m => this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), - ))).filter(x => x != null) as MiUser[]; + ))).filter(isNotNull); // Drop duplicate users mentionedUsers = mentionedUsers.filter((u, i, self) => diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index d47be79441..0fccc7b950 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -8,6 +8,7 @@ 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'; @@ -40,7 +41,7 @@ export class ApAudienceService { const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), - )).filter((x): x is MiUser => x != null); + )).filter(isNotNull); 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 1cc54b6ff6..8d9cd74a25 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -27,6 +27,7 @@ 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 { 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'; @@ -521,7 +522,7 @@ export class ApInboxService { const userIds = uris .filter(uri => uri.startsWith(this.config.url + '/users/')) .map(uri => uri.split('/').at(-1)) - .filter((userId): userId is string => userId !== undefined); + .filter(isNotNull); const users = await this.usersRepository.findBy({ id: In(userIds), }); diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 494622909a..d7fb977a99 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -315,7 +315,7 @@ export class ApRendererService { const getPromisedFiles = async (ids: string[]): Promise => { if (ids.length === 0) return []; const items = await this.driveFilesRepository.findBy({ id: In(ids) }); - return ids.map(id => items.find(item => item.id === id)).filter((item): item is MiDriveFile => item != null); + return ids.map(id => items.find(item => item.id === id)).filter(isNotNull); }; let inReplyTo; diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts index 73eea1edf0..0ced7e88af 100644 --- a/packages/backend/src/core/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts @@ -8,6 +8,7 @@ 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'; @@ -27,7 +28,7 @@ export class ApMentionService { const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), - )).filter((x): x is MiUser => x != null); + )).filter(isNotNull); return mentionedUsers; } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 8da9407216..e201b88173 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -37,6 +37,7 @@ import { ApQuestionService } from './ApQuestionService.js'; import { ApImageService } from './ApImageService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IPost } from '../type.js'; +import { isNotNull } from '@/misc/is-not-null.js'; @Injectable() export class ApNoteService { @@ -221,7 +222,7 @@ export class ApNoteService { } }; - const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); + const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull)); 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 e80cd34a56..744b1ea683 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -38,6 +38,7 @@ 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'; @@ -636,7 +637,7 @@ export class ApPersonService implements OnModuleInit { // とりあえずidを別の時間で生成して順番を維持 let td = 0; - for (const note of featuredNotes.filter((note): note is MiNote => note != null)) { + for (const note of featuredNotes.filter(isNotNull)) { 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 e78b3a3599..d1936cfe1d 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -10,6 +10,7 @@ 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'; @@ -51,7 +52,7 @@ export class ApQuestionService { const choices = question[multiple ? 'anyOf' : 'oneOf'] ?.map((x) => x.name) - .filter((x): x is string => typeof x === 'string') + .filter(isNotNull) ?? []; const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); diff --git a/packages/backend/src/core/activitypub/models/tag.ts b/packages/backend/src/core/activitypub/models/tag.ts index ced101b764..e7ceec3262 100644 --- a/packages/backend/src/core/activitypub/models/tag.ts +++ b/packages/backend/src/core/activitypub/models/tag.ts @@ -4,6 +4,7 @@ */ 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'; @@ -15,7 +16,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined): return hashtags.map(tag => { const m = tag.name.match(/^#(.+)/); return m ? m[1] : null; - }).filter((x): x is string => x != null); + }).filter(isNotNull); } export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 50f1c49b48..8affe2b3bf 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -259,7 +259,7 @@ export class DriveFileEntityService { options?: PackOptions, ): Promise[]> { const items = await Promise.all(files.map(f => this.packNullable(f, options))); - return items.filter((x): x is Packed<'DriveFile'> => x != null); + return items.filter(isNotNull); } @bindThis diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts index fe7b137bd2..65c69a49a7 100644 --- a/packages/backend/src/core/entities/PageEntityService.ts +++ b/packages/backend/src/core/entities/PageEntityService.ts @@ -14,6 +14,7 @@ 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'; @@ -102,7 +103,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((x): x is MiDriveFile => x != null)), + attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)), likedCount: page.likedCount, isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined, }); diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 53df32f210..14761357a5 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -25,6 +25,7 @@ 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'; @@ -384,7 +385,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(x => x != null) as string[]) + .then(xs => xs.length === 0 ? null : xs.filter(isNotNull)) : null, createdAt: this.idService.parse(user.id).date.toISOString(), updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, diff --git a/packages/backend/src/misc/is-not-null.ts b/packages/backend/src/misc/is-not-null.ts index 584a09d35a..8d9dc8bb39 100644 --- a/packages/backend/src/misc/is-not-null.ts +++ b/packages/backend/src/misc/is-not-null.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -// we are using {} as "any non-nullish value" as expected -// eslint-disable-next-line @typescript-eslint/ban-types -export function isNotNull(input: T | undefined | null): input is T { +export function isNotNull>(input: T | undefined | null): input is T { return input != null; } 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 784ae5088f..b07cdf1ed9 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -12,6 +12,7 @@ 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'], @@ -69,7 +70,7 @@ export default class extends Endpoint { // eslint- id: fileId, userId: me.id, }), - ))).filter((file): file is MiDriveFile => file != null); + ))).filter(isNotNull); 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 8872b261dd..8bd83ff5ba 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -10,6 +10,7 @@ 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'], @@ -67,7 +68,7 @@ export default class extends Endpoint { // eslint- id: fileId, userId: me.id, }), - ))).filter((file): file is MiDriveFile => file != null); + ))).filter(isNotNull); if (files.length === 0) { throw new Error(); diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 1f4509764f..784766bcb5 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -12,6 +12,7 @@ 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'], @@ -52,7 +53,7 @@ export default class extends Endpoint { // eslint- host: acct.host ?? IsNull(), }))); - return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users.filter(isNotNull), me, { schema: 'UserDetailed' }); }); } } From 30fe0726069a90fe1ced88a8e4fbdec19fe13078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:13:46 +0900 Subject: [PATCH 16/78] =?UTF-8?q?fix(test):=20Chromatic=E3=81=8C=E8=90=BD?= =?UTF-8?q?=E3=81=A1=E3=81=A6=E3=81=84=E3=82=8B=E3=81=AE=E3=82=92=E4=B8=80?= =?UTF-8?q?=E9=83=A8=E4=BF=AE=E6=AD=A3=EF=BC=9F=20(#13435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(test): Chromaticが落ちているのを修正? * いらん変更をけす * 未来過ぎた --- .../frontend/src/components/global/MkTime.stories.impl.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts index 2b4b1485fd..8ddf8e213a 100644 --- a/packages/frontend/src/components/global/MkTime.stories.impl.ts +++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts @@ -10,7 +10,7 @@ import MkTime from './MkTime.vue'; import { i18n } from '@/i18n.js'; import { dateTimeFormat } from '@/scripts/intl-const.js'; const now = new Date('2023-04-01T00:00:00.000Z'); -const future = new Date(8640000000000000); +const future = new Date('3000-04-01T00:00:00.000Z'); const oneHourAgo = new Date(now.getTime() - 3600000); const oneDayAgo = new Date(now.getTime() - 86400000); const oneWeekAgo = new Date(now.getTime() - 604800000); @@ -49,11 +49,12 @@ export const Empty = { export const RelativeFuture = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.ts._ago.future); + await expect(canvasElement).toHaveTextContent(i18n.tsx._timeIn.years({ n: 977 })); }, args: { ...Empty.args, time: future, + origin: now, }, } satisfies StoryObj; export const AbsoluteFuture = { From a85fccaeea93d610d8cdd52def77851166a9391c Mon Sep 17 00:00:00 2001 From: 1Step621 <86859447+1STEP621@users.noreply.github.com> Date: Fri, 23 Feb 2024 17:01:42 +0900 Subject: [PATCH 17/78] =?UTF-8?q?Fix(frontend):=20=E7=B5=B5=E6=96=87?= =?UTF-8?q?=E5=AD=97=E3=82=AA=E3=83=BC=E3=83=88=E3=82=B3=E3=83=B3=E3=83=97?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E3=83=88=E3=81=AE=E5=84=AA=E5=85=88=E9=A0=86?= =?UTF-8?q?=E4=BD=8D=E3=81=8C=E3=81=8A=E3=81=8B=E3=81=97=E3=81=84=E3=81=AE?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=20(#13423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 絵文字オートコンプリートの優先順位がおかしいのを修正 * update CHANGELOG.md * テストを追加 * lint fix --- CHANGELOG.md | 1 + .../src/components/MkAutocomplete.vue | 96 +---------------- packages/frontend/src/scripts/search-emoji.ts | 101 ++++++++++++++++++ packages/frontend/test/autocomplete.test.ts | 34 ++++++ 4 files changed, 138 insertions(+), 94 deletions(-) create mode 100644 packages/frontend/src/scripts/search-emoji.ts create mode 100644 packages/frontend/test/autocomplete.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff5881df0..a939fa7621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正 - Fix: チャートのラベルが消えている問題を修正 - Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正 +- Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正 ### Server - Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正 diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 412325bfee..cae6bc7111 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -57,18 +57,7 @@ import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; import { MFM_TAGS, MFM_PARAMS } from '@/const.js'; - -type EmojiDef = { - emoji: string; - name: string; - url: string; - aliasOf?: string; -} | { - emoji: string; - name: string; - aliasOf?: string; - isCustomEmoji?: true; -}; +import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js'; const lib = emojilist.filter(x => x.category !== 'flags'); @@ -249,7 +238,7 @@ function exec() { return; } - emojis.value = emojiAutoComplete(props.q, emojiDb.value); + emojis.value = searchEmoji(props.q, emojiDb.value); } else if (props.type === 'mfmTag') { if (!props.q || props.q === '') { mfmTags.value = MFM_TAGS; @@ -267,87 +256,6 @@ function exec() { } } -type EmojiScore = { emoji: EmojiDef, score: number }; - -function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] { - if (!query) { - return []; - } - - const matched = new Map(); - // 完全一致(エイリアス込み) - emojiDb.some(x => { - if (x.name === query && !matched.has(x.aliasOf ?? x.name)) { - matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 }); - } - return matched.size === max; - }); - - // 前方一致(エイリアスなし) - if (matched.size < max) { - emojiDb.some(x => { - if (x.name.startsWith(query) && !x.aliasOf) { - matched.set(x.name, { emoji: x, score: query.length + 1 }); - } - return matched.size === max; - }); - } - - // 前方一致(エイリアス込み) - if (matched.size < max) { - emojiDb.some(x => { - if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) { - matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length }); - } - return matched.size === max; - }); - } - - // 部分一致(エイリアス込み) - if (matched.size < max) { - emojiDb.some(x => { - if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) { - matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 }); - } - return matched.size === max; - }); - } - - // 簡易あいまい検索(3文字以上) - if (matched.size < max && query.length > 3) { - const queryChars = [...query]; - const hitEmojis = new Map(); - - for (const x of emojiDb) { - // 文字列の位置を進めながら、クエリの文字を順番に探す - - let pos = 0; - let hit = 0; - for (const c of queryChars) { - pos = x.name.indexOf(c, pos); - if (pos <= -1) break; - hit++; - } - - // 半分以上の文字が含まれていればヒットとする - if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) { - hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 }); - } - } - - // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分) - [...hitEmojis.values()] - .sort((x, y) => y.score - x.score) - .slice(0, 6) - .forEach(it => matched.set(it.emoji.name, it)); - } - - return [...matched.values()] - .sort((x, y) => y.score - x.score) - .slice(0, max) - .map(it => it.emoji); -} - function onMousedown(event: Event) { if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close(); } diff --git a/packages/frontend/src/scripts/search-emoji.ts b/packages/frontend/src/scripts/search-emoji.ts new file mode 100644 index 0000000000..07f55e5531 --- /dev/null +++ b/packages/frontend/src/scripts/search-emoji.ts @@ -0,0 +1,101 @@ +export type EmojiDef = { + emoji: string; + name: string; + url: string; + aliasOf?: string; +} | { + emoji: string; + name: string; + aliasOf?: string; + isCustomEmoji?: true; +}; +type EmojiScore = { emoji: EmojiDef, score: number }; + +export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] { + if (!query) { + return []; + } + + const matched = new Map(); + // 完全一致(エイリアスなし) + emojiDb.some(x => { + if (x.name === query && !x.aliasOf) { + matched.set(x.name, { emoji: x, score: query.length + 3 }); + } + return matched.size === max; + }); + + // 完全一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name === query && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 }); + } + return matched.size === max; + }); + } + + // 前方一致(エイリアスなし) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.startsWith(query) && !x.aliasOf && !matched.has(x.name)) { + matched.set(x.name, { emoji: x, score: query.length + 1 }); + } + return matched.size === max; + }); + } + + // 前方一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length }); + } + return matched.size === max; + }); + } + + // 部分一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 }); + } + return matched.size === max; + }); + } + + // 簡易あいまい検索(3文字以上) + if (matched.size < max && query.length > 3) { + const queryChars = [...query]; + const hitEmojis = new Map(); + + for (const x of emojiDb) { + // 文字列の位置を進めながら、クエリの文字を順番に探す + + let pos = 0; + let hit = 0; + for (const c of queryChars) { + pos = x.name.indexOf(c, pos); + if (pos <= -1) break; + hit++; + } + + // 半分以上の文字が含まれていればヒットとする + if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) { + hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 }); + } + } + + // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分) + [...hitEmojis.values()] + .sort((x, y) => y.score - x.score) + .slice(0, 6) + .forEach(it => matched.set(it.emoji.name, it)); + } + + return [...matched.values()] + .sort((x, y) => y.score - x.score) + .slice(0, max) + .map(it => it.emoji); +} diff --git a/packages/frontend/test/autocomplete.test.ts b/packages/frontend/test/autocomplete.test.ts new file mode 100644 index 0000000000..f6a7ce9450 --- /dev/null +++ b/packages/frontend/test/autocomplete.test.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { assert, describe, test } from 'vitest'; +import { searchEmoji } from '@/scripts/search-emoji.js'; + +describe('emoji autocomplete', () => { + test('名前の完全一致は名前の前方一致より優先される', async () => { + const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]); + assert.equal(result[0].emoji, ':foooo:'); + }); + + test('名前の前方一致は名前の部分一致より優先される', async () => { + const result = searchEmoji('baaa', [{ emoji: ':baaar:', name: 'baaar' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]); + assert.equal(result[0].emoji, ':baaar:'); + }); + + test('名前の完全一致はタグの完全一致より優先される', async () => { + const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]); + assert.equal(result[0].emoji, ':foooo:'); + }); + + test('名前の前方一致はタグの前方一致より優先される', async () => { + const result = searchEmoji('foo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]); + assert.equal(result[0].emoji, ':foooo:'); + }); + + test('名前の部分一致はタグの部分一致より優先される', async () => { + const result = searchEmoji('oooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]); + assert.equal(result[0].emoji, ':foooo:'); + }); +}); From b8d8b359bc8a6c542d78b86f500e0f45f63f48fb Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 23 Feb 2024 17:19:08 +0900 Subject: [PATCH 18/78] =?UTF-8?q?fix:=20=E3=83=97=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E9=80=9A=E7=9F=A5=E3=81=AE=E5=A4=89=E6=9B=B4=E3=81=8C?= =?UTF-8?q?1=E6=99=82=E9=96=93=E3=81=BB=E3=81=A9=E5=8F=8D=E6=98=A0?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#13407)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: プッシュ通知の変更が1時間ほど反映されない問題を修正 * 410 to refresh * refreshCache --- packages/backend/src/core/PushNotificationService.ts | 7 +++++++ packages/backend/src/server/api/endpoints/sw/register.ts | 4 ++++ packages/backend/src/server/api/endpoints/sw/unregister.ts | 7 +++++++ .../src/server/api/endpoints/sw/update-registration.ts | 5 +++++ 4 files changed, 23 insertions(+) diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index e630539fbc..3b706d9854 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -115,12 +115,19 @@ export class PushNotificationService implements OnApplicationShutdown { endpoint: subscription.endpoint, auth: subscription.auth, publickey: subscription.publickey, + }).then(() => { + this.refreshCache(userId); }); } }); } } + @bindThis + public refreshCache(userId: string): void { + this.subscriptionsCache.refresh(userId); + } + @bindThis public dispose(): void { this.subscriptionsCache.dispose(); diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 06c04b3f9a..a9a33149f9 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -9,6 +9,7 @@ import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; +import { PushNotificationService } from '@/core/PushNotificationService.js'; export const meta = { tags: ['account'], @@ -66,6 +67,7 @@ export default class extends Endpoint { // eslint- private idService: IdService, private metaService: MetaService, + private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { // if already subscribed @@ -97,6 +99,8 @@ export default class extends Endpoint { // eslint- sendReadMessage: ps.sendReadMessage, }); + this.pushNotificationService.refreshCache(me.id); + return { state: 'subscribed' as const, key: instance.swPublicKey, diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index 2bc91c7278..2edf7fab1b 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { PushNotificationService } from '@/core/PushNotificationService.js'; export const meta = { tags: ['account'], @@ -29,12 +30,18 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, + + private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { await this.swSubscriptionsRepository.delete({ ...(me ? { userId: me.id } : {}), endpoint: ps.endpoint, }); + + if (me) { + this.pushNotificationService.refreshCache(me.id); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts index b56b07fd00..839a07c770 100644 --- a/packages/backend/src/server/api/endpoints/sw/update-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { PushNotificationService } from '@/core/PushNotificationService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -58,6 +59,8 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, + + private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { const swSubscription = await this.swSubscriptionsRepository.findOneBy({ @@ -77,6 +80,8 @@ export default class extends Endpoint { // eslint- sendReadMessage: swSubscription.sendReadMessage, }); + this.pushNotificationService.refreshCache(me.id); + return { userId: swSubscription.userId, endpoint: swSubscription.endpoint, From a861f913a772841d69d6b19aaa85c3985e3e073c Mon Sep 17 00:00:00 2001 From: okayurisotto <47853651+okayurisotto@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:02:12 +0900 Subject: [PATCH 19/78] =?UTF-8?q?fix(backend):=20=E3=82=88=E3=82=8A?= =?UTF-8?q?=E5=A4=9A=E3=81=8F=E3=81=AE=E4=BA=BA=E3=81=AB=E4=BD=BF=E3=82=8F?= =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8B=E3=83=8F=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=82=BF=E3=82=B0=E3=81=8C=E6=A4=9C=E7=B4=A2=E7=B5=90?= =?UTF-8?q?=E6=9E=9C=E4=B8=8A=E4=BD=8D=E3=81=AB=E6=9D=A5=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=20(#11498)=20(#13340)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/api/endpoints/hashtags/search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index 12d47fa512..d4eb851054 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -43,7 +43,7 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' }) - .orderBy('tag.count', 'DESC') + .orderBy('tag.mentionedLocalUsersCount', 'DESC') .groupBy('tag.id') .limit(ps.limit) .offset(ps.offset) From 600d91beda206fa22cfa1c1a3f94ca9e5a0cac68 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 23 Feb 2024 18:04:30 +0900 Subject: [PATCH 20/78] =?UTF-8?q?enhance:=20=E3=83=AA=E3=83=A2=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=AE=E3=83=95=E3=82=A9=E3=83=AD=E3=83=AF=E3=83=BC?= =?UTF-8?q?=E3=81=8B=E3=82=89=E5=86=8D=E5=BA=A6Follow=E3=81=8C=E6=9D=A5?= =?UTF-8?q?=E3=81=9F=E5=A0=B4=E5=90=88=E3=80=81accept=E3=82=92=E8=BF=94?= =?UTF-8?q?=E3=81=97=E3=81=A6=E3=81=82=E3=81=92=E3=82=8B=20(#13388)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance: リモートのフォロワーから再度Followが来た場合、acceptを返してあげる * nanka meccha kaeta * ブロックチェックの後にフォロー関係の存在チェックをする --- .../backend/src/core/UserFollowingService.ts | 60 +++++++++++++++---- .../RelationshipProcessorService.ts | 2 +- .../server/api/endpoints/following/create.ts | 13 +--- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 8ad85391c6..d87cbacdcb 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -30,6 +30,7 @@ import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import type { ThinUser } from '@/queue/types.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -94,20 +95,43 @@ export class UserFollowingService implements OnModuleInit { this.userBlockingService = this.moduleRef.get('UserBlockingService'); } + @bindThis + public async deliverAccept(follower: MiRemoteUser, followee: MiPartialLocalUser, requestId?: string) { + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + this.queueService.deliver(followee, content, follower.inbox, false); + } + + /** + * ThinUserでなくともユーザーの情報が最新でない場合はこちらを使うべき + */ + @bindThis + public async followByThinUser( + _follower: ThinUser, + _followee: ThinUser, + options: Parameters[2] = {}, + ) { + const [follower, followee] = await Promise.all([ + this.usersRepository.findOneByOrFail({ id: _follower.id }), + this.usersRepository.findOneByOrFail({ id: _followee.id }), + ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; + + await this.follow(follower, followee, options); + } + @bindThis public async follow( - _follower: { id: MiUser['id'] }, - _followee: { id: MiUser['id'] }, + follower: MiLocalUser | MiRemoteUser, + followee: MiLocalUser | MiRemoteUser, { requestId, silent = false, withReplies }: { requestId?: string, silent?: boolean, withReplies?: boolean, } = {}, ): Promise { - const [follower, followee] = await Promise.all([ - this.usersRepository.findOneByOrFail({ id: _follower.id }), - this.usersRepository.findOneByOrFail({ id: _followee.id }), - ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isRemoteUser(followee)) { + // What? + throw new Error('Remote user cannot follow remote user.'); + } // check blocking const [blocking, blocked] = await Promise.all([ @@ -129,6 +153,24 @@ export class UserFollowingService implements OnModuleInit { if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); } + if (await this.followingsRepository.exists({ + where: { + followerId: follower.id, + followeeId: followee.id, + }, + })) { + // すでにフォロー関係が存在している場合 + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + // リモート → ローカル: acceptを送り返しておしまい + this.deliverAccept(follower, followee, requestId); + return; + } + if (this.userEntityService.isLocalUser(follower)) { + // ローカル → リモート/ローカル: 例外 + throw new IdentifiableError('ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced', 'already following'); + } + } + const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id }); // フォロー対象が鍵アカウントである or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or @@ -189,8 +231,7 @@ export class UserFollowingService implements OnModuleInit { await this.insertFollowingDoc(followee, follower, silent, withReplies); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); - this.queueService.deliver(followee, content, follower.inbox, false); + this.deliverAccept(follower, followee, requestId); } } @@ -571,8 +612,7 @@ export class UserFollowingService implements OnModuleInit { await this.insertFollowingDoc(followee, follower, false, request.withReplies); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee)); - this.queueService.deliver(followee, content, follower.inbox, false); + this.deliverAccept(follower, followee as MiPartialLocalUser, request.requestId ?? undefined); } this.userEntityService.pack(followee.id, followee, { diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts index 408b02fb38..53dbb42169 100644 --- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts +++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts @@ -35,7 +35,7 @@ export class RelationshipProcessorService { @bindThis public async processFollow(job: Bull.Job): Promise { this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? "with replies" : "without replies"}`); - await this.userFollowingService.follow(job.data.from, job.data.to, { + await this.userFollowingService.followByThinUser(job.data.from, job.data.to, { requestId: job.data.requestId, silent: job.data.silent, withReplies: job.data.withReplies, diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index ceaf32ccb2..042d7f119d 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -100,22 +100,11 @@ export default class extends Endpoint { // eslint- throw err; }); - // Check if already following - const exist = await this.followingsRepository.exists({ - where: { - followerId: follower.id, - followeeId: followee.id, - }, - }); - - if (exist) { - throw new ApiError(meta.errors.alreadyFollowing); - } - try { await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies }); } catch (e) { if (e instanceof IdentifiableError) { + if (e.id === 'ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced') throw new ApiError(meta.errors.alreadyFollowing); if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking); if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked); } From d8342322327924a111a8ece0a6c7eb8c9ac2f378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:07:41 +0900 Subject: [PATCH 21/78] =?UTF-8?q?enhance(games):=20=E6=8A=9C=E3=81=91?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=82=8B=E7=BF=BB=E8=A8=B3=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=83=BB=E3=82=B9=E3=82=BF=E3=82=A4=E3=83=AB=E5=85=B1?= =?UTF-8?q?=E9=80=9A=E5=8C=96=20(#13434)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(games): 抜けている翻訳を追加・スタイル共通化 * frameDivider の使用箇所が見当たらなかったので削除 * ミス * インナーでもcss変数を使う * コロンを翻訳から外す * 一部の翻訳を除去 * p * revert some text --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- locales/index.d.ts | 62 +++++++++ locales/ja-JP.yml | 16 +++ .../src/pages/drop-and-fusion.game.vue | 120 ++++++++---------- .../frontend/src/pages/drop-and-fusion.vue | 56 ++------ .../frontend/src/pages/reversi/game.board.vue | 17 +-- packages/frontend/src/style.scss | 33 +++++ 6 files changed, 177 insertions(+), 127 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index d483fea837..1a2565b06a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4856,6 +4856,14 @@ export interface Locale extends ILocale { * リプレイ中 */ "replaying": string; + /** + * リプレイを終了 + */ + "endReplay": string; + /** + * リプレイデータをコピー + */ + "copyReplayData": string; /** * ランキング */ @@ -4884,11 +4892,57 @@ export interface Locale extends ILocale { * スワイプしてタブを切り替える */ "enableHorizontalSwipe": string; + /** + * 読み込み中 + */ + "loading": string; + /** + * やめる + */ + "surrender": string; + /** + * リトライ + */ + "gameRetry": string; "_bubbleGame": { /** * 遊び方 */ "howToPlay": string; + /** + * ホールド + */ + "hold": string; + "_score": { + /** + * スコア + */ + "score": string; + /** + * 稼いだ金額 + */ + "scoreYen": string; + /** + * ハイスコア + */ + "highScore": string; + /** + * 最大チェーン数 + */ + "maxChain": string; + /** + * {yen}円 + */ + "yen": ParameterizedString<"yen">; + /** + * {qty}個分 + */ + "estimatedQty": ParameterizedString<"qty">; + /** + * おにぎり {onigiriQtyWithUnit} + */ + "scoreSweets": ParameterizedString<"onigiriQtyWithUnit">; + }; "_howToPlay": { /** * 位置を調整してハコにモノを落とします。 @@ -9659,6 +9713,14 @@ export interface Locale extends ILocale { * 変則なし */ "disallowIrregularRules": string; + /** + * 盤面に行・列番号を表示 + */ + "showBoardLabels": string; + /** + * 石をアイコンにする + */ + "useAvatarAsStone": string; }; "_offlineScreen": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7e16619fc7..61c61b8f96 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1210,6 +1210,8 @@ soundWillBePlayed: "サウンドが再生されます" showReplay: "リプレイを見る" replay: "リプレイ" replaying: "リプレイ中" +endReplay: "リプレイを終了" +copyReplayData: "リプレイデータをコピー" ranking: "ランキング" lastNDays: "直近{n}日" backToTitle: "タイトルへ" @@ -1217,9 +1219,21 @@ hemisphere: "お住まいの地域" withSensitive: "センシティブなファイルを含むノートを表示" userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" enableHorizontalSwipe: "スワイプしてタブを切り替える" +loading: "読み込み中" +surrender: "やめる" +gameRetry: "リトライ" _bubbleGame: howToPlay: "遊び方" + hold: "ホールド" + _score: + score: "スコア" + scoreYen: "稼いだ金額" + highScore: "ハイスコア" + maxChain: "最大チェーン数" + yen: "{yen}円" + estimatedQty: "{qty}個分" + scoreSweets: "おにぎり {onigiriQtyWithUnit}" _howToPlay: section1: "位置を調整してハコにモノを落とします。" section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。" @@ -2572,6 +2586,8 @@ _reversi: opponentHasSettingsChanged: "相手が設定を変更しました" allowIrregularRules: "変則許可 (完全フリー)" disallowIrregularRules: "変則なし" + showBoardLabels: "盤面に行・列番号を表示" + useAvatarAsStone: "石をアイコンにする" _offlineScreen: title: "オフライン - サーバーに接続できません" diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index d9881cebbf..eba5b92154 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -7,9 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
- Loading... -
+
{{ i18n.ts.loading }}
@@ -32,18 +30,18 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
- BUBBLE GAME -
- {{ gameMode }} -
+
+
+ {{ i18n.ts.bubbleGame }} +
- {{ gameMode.toUpperCase() }} -
-
-
- HOLD +
+
+ {{ i18n.ts._bubbleGame.hold }}
-
+
-
SCORE: {{ getScoreUnit(gameMode) }}
-
MAX CHAIN:
-
TOTAL EARNINGS:
-
おにぎり個分
+
{{ i18n.ts._bubbleGame._score.score }}: {{ getScoreUnit(gameMode) }}
+
{{ i18n.ts._bubbleGame._score.maxChain }}:
+
+ {{ i18n.ts._bubbleGame._score.scoreYen }}: + + + +
+ + +
{{ i18n.ts.replaying }}
-
-
+
+
-
+
- END + {{ i18n.ts.endReplay }} x4 x16
-
-
+
+
{{ i18n.ts.backToTitle }} {{ i18n.ts.showReplay }} {{ i18n.ts.share }} - Copy replay data + {{ i18n.ts.copyReplayData }}
-
-
-
SCORE: {{ getScoreUnit(gameMode) }}
-
HIGH SCORE: {{ getScoreUnit(gameMode) }}-
-
TOTAL EARNINGS: -
+
+
+
{{ i18n.ts._bubbleGame._score.score }}: {{ getScoreUnit(gameMode) }}
+
{{ i18n.ts._bubbleGame._score.highScore }}: {{ getScoreUnit(gameMode) }}-
+
+ {{ i18n.ts._bubbleGame._score.scoreYen }}: + + + +
-
-
+
+
-
-
+
+
@@ -153,8 +167,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
+
+
FUSION RECIPE
@@ -165,10 +179,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
- Surrender - Retry +
+
+ {{ i18n.ts.surrender }} + {{ i18n.ts.gameRetry }}
@@ -1313,38 +1327,6 @@ definePageMetadata(() => ({ max-width: 100%; } -.frame { - padding: 7px; - background: #8C4F26; - box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c; - border-radius: 10px; -} - -.frameH { - display: flex; - gap: 6px; -} - -.frameInner { - padding: 8px; - margin-top: 8px; - background: #F1E8DC; - box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410; - border-radius: 6px; - color: #693410; - - &:first-child { - margin-top: 0; - } -} - -.frameDivider { - height: 0; - border: none; - border-top: 1px solid #693410; - border-bottom: 1px solid #ce8a5c; -} - .header { position: relative; z-index: 10; diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 1b11457988..54352c9b0d 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -15,13 +15,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
+
+
-
-
+
+
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.start }}
-
+
{{ i18n.ts.soundWillBePlayed }}
@@ -42,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
+
+
-
{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }} ({{ gameMode }})
+
{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }} ({{ gameMode.toUpperCase() }})
@@ -57,8 +57,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
+
+
{{ i18n.ts._bubbleGame.howToPlay }}
  1. {{ i18n.ts._bubbleGame._howToPlay.section1 }}
  2. @@ -67,8 +67,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
+
+
Credit
@@ -149,38 +149,6 @@ definePageMetadata(() => ({ } } -.frame { - padding: 7px; - background: #8C4F26; - box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c; - border-radius: 10px; -} - -.frameH { - display: flex; - gap: 6px; -} - -.frameInner { - padding: 8px; - margin-top: 8px; - background: #F1E8DC; - box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410; - border-radius: 6px; - color: #693410; - - &:first-child { - margin-top: 0; - } -} - -.frameDivider { - height: 0; - border: none; - border-top: 1px solid #693410; - border-bottom: 1px solid #ce8a5c; -} - .rankingRecord { display: flex; line-height: 24px; diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 6f7f5b8f38..5259dfa29a 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ String.fromCharCode(64 + i) }} @@ -124,8 +124,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- Show labels - useAvatarAsStone + {{ i18n.ts._reversi.showBoardLabels }} + {{ i18n.ts._reversi.useAvatarAsStone }}
@@ -500,17 +500,6 @@ $gap: 4px; text-align: center; } -.board { - width: 100%; - box-sizing: border-box; - margin: 0 auto; - - padding: 7px; - background: #8C4F26; - box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c; - border-radius: 12px; -} - .boardInner { padding: 32px; diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index cbec377277..0951a7d98d 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -417,6 +417,39 @@ rt { transition-timing-function: cubic-bezier(0,.5,.5,1); } +._woodenFrame { + padding: 7px; + background: #8C4F26; + box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c; + border-radius: 10px; + + --bg: #F1E8DC; + --panel: #fff; + --fg: #693410; + --switchOffBg: rgba(0, 0, 0, 0.1); + --switchOffFg: rgb(255, 255, 255); + --switchOnBg: var(--accent); + --switchOnFg: rgb(255, 255, 255); +} + +._woodenFrameH { + display: flex; + gap: 6px; +} + +._woodenFrameInner { + padding: 8px; + margin-top: 8px; + background: var(--bg); + box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410; + border-radius: 6px; + color: var(--fg); + + &:first-child { + margin-top: 0; + } +} + ._transition_zoom-enter-active, ._transition_zoom-leave-active { transition: opacity 0.5s, transform 0.5s !important; } From c0156b740b6ce87f2cc55aa85f9d828ef41342ee Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 23 Feb 2024 18:15:39 +0900 Subject: [PATCH 22/78] =?UTF-8?q?enhance=3F:=20DeleteAccountService?= =?UTF-8?q?=E3=81=A7=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4=E3=81=99=E3=82=8B=E9=9A=9B=E3=81=ABuserChangeDeletedS?= =?UTF-8?q?tate=E3=82=92=E7=99=BA=E8=A1=8C=E3=81=99=E3=82=8B=20(#13382)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/CacheService.ts | 1 + packages/backend/src/core/DeleteAccountService.ts | 4 ++++ packages/backend/src/core/GlobalEventService.ts | 1 + packages/backend/src/core/activitypub/ApInboxService.ts | 1 - 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 263df56476..0fc47bf8ec 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -128,6 +128,7 @@ export class CacheService implements OnApplicationShutdown { const { type, body } = obj.message as GlobalEvents['internal']['payload']; switch (type) { case 'userChangeSuspendedState': + case 'userChangeDeletedState': case 'remoteUserUpdated': { const user = await this.usersRepository.findOneBy({ id: body.id }); if (user == null) { diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index fc5d217ae0..79b614edba 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -9,6 +9,7 @@ import { QueueService } from '@/core/QueueService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; @Injectable() export class DeleteAccountService { @@ -18,6 +19,7 @@ export class DeleteAccountService { private userSuspendService: UserSuspendService, private queueService: QueueService, + private globalEventService: GlobalEventService, ) { } @@ -39,5 +41,7 @@ export class DeleteAccountService { await this.usersRepository.update(user.id, { isDeleted: true, }); + + this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true }); } } diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 01dd133ead..a127a6df3b 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -209,6 +209,7 @@ type SerializedAll = { export interface InternalEventTypes { userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; + userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; }; userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; }; remoteUserUpdated: { id: MiUser['id']; }; follow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 8d9cd74a25..b0f56a5d82 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -85,7 +85,6 @@ export class ApInboxService { private apPersonService: ApPersonService, private apQuestionService: ApQuestionService, private queueService: QueueService, - private cacheService: CacheService, private globalEventService: GlobalEventService, ) { this.logger = this.apLoggerService.logger; From e3dd3f6b63efffde6dd125e8ecef66aa7069c1a0 Mon Sep 17 00:00:00 2001 From: 1Step621 <86859447+1STEP621@users.noreply.github.com> Date: Sat, 24 Feb 2024 10:22:23 +0900 Subject: [PATCH 23/78] =?UTF-8?q?Enhance(frontend):=20=E3=83=AA=E3=82=A2?= =?UTF-8?q?=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=94=E3=83=83=E3=82=AB?= =?UTF-8?q?=E3=83=BC=E3=82=92=E8=AA=BF=E6=95=B4=20(#13354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 打てない絵文字を表示しないのではなくグレーアウトするように など * fix: 今度は検索とピン留めに効いてなかった * lint fix * use Map * 斜めに線を引いてわかりやすく * 斜め線は右上からのほうが良かったかも * デザイン調整 --- .../src/components/MkEmojiPicker.section.vue | 3 + .../frontend/src/components/MkEmojiPicker.vue | 86 ++++++++++++++++--- .../components/MkReactionsViewer.reaction.vue | 11 ++- .../src/scripts/check-reaction-permissions.ts | 6 +- packages/frontend/src/scripts/emojilist.ts | 4 + 5 files changed, 90 insertions(+), 20 deletions(-) diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 30ad2bcbbf..c295ab6bb7 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only :key="emoji" :data-emoji="emoji" class="_button item" + :disabled="disabledEmojis?.value.includes(emoji)" @pointerenter="computeButtonTitle" @click="emit('chosen', emoji, $event)" > @@ -48,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only :key="emoji" :data-emoji="emoji" class="_button item" + :disabled="disabledEmojis?.value.includes(emoji)" @pointerenter="computeButtonTitle" @click="emit('chosen', emoji, $event)" > @@ -67,6 +69,7 @@ import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue'; const props = defineProps<{ emojis: string[] | Ref; + disabledEmojis?: Ref; initialShown?: boolean; hasChildSection?: boolean; customEmojiTree?: CustomEmojiFolderTree[]; diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 366273118b..061afa66ac 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="emoji in searchResultCustom" :key="emoji.name" class="_button item" + :disabled="!canReact(emoji)" :title="emoji.name" tabindex="0" @click="chosen(emoji, $event)" @@ -39,16 +40,17 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -57,15 +59,16 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.recentUsed }}
@@ -76,7 +79,8 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="child in customEmojiFolderRoot.children" :key="`custom:${child.value}`" :initialShown="false" - :emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))" + :emojis="computed(() => customEmojis.filter(e => filterCategory(e, child.value)).map(e => `:${e.name}:`))" + :disabledEmojis="computed(() => customEmojis.filter(e => filterCategory(e, child.value)).filter(e => !canReact(e)).map(e => `:${e.name}:`))" :hasChildSection="child.children.length !== 0" :customEmojiTree="child.children" @chosen="chosen" @@ -104,6 +108,7 @@ import * as Misskey from 'misskey-js'; import XSection from '@/components/MkEmojiPicker.section.vue'; import { emojilist, + unicodeEmojisMap, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, @@ -146,6 +151,13 @@ const { recentlyUsedEmojis, } = defaultStore.reactiveState; +const recentlyUsedEmojisDef = computed(() => { + return recentlyUsedEmojis.value.map(getDef); +}); +const pinnedEmojisDef = computed(() => { + return pinned.value?.map(getDef); +}); + const pinned = computed(() => props.pinnedEmojis); const size = computed(() => emojiPickerScale.value); const width = computed(() => emojiPickerWidth.value); @@ -337,14 +349,18 @@ watch(q, () => { return matches; }; - searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable); + searchResultCustom.value = Array.from(searchCustom()); searchResultUnicode.value = Array.from(searchUnicode()); }); -function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean { +function canReact(emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef): boolean { return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji); } +function filterCategory(emoji: Misskey.entities.EmojiSimple, category: string): boolean { + return category === '' ? (emoji.category === 'null' || !emoji.category) : emoji.category === category; +} + function focus() { if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) { searchEl.value?.focus({ @@ -362,6 +378,14 @@ function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef): return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`; } +function getDef(emoji: string) { + if (emoji.includes(':')) { + return customEmojisMap.get(emoji.replace(/:/g, ''))!; + } else { + return unicodeEmojisMap.get(emoji)!; + } +} + /** @see MkEmojiPicker.section.vue */ function computeButtonTitle(ev: MouseEvent): void { const elm = ev.target as HTMLElement; @@ -526,6 +550,18 @@ defineExpose({ width: auto; height: auto; min-width: 0; + + &:disabled { + cursor: not-allowed; + background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + opacity: 1; + + > .emoji { + filter: grayscale(1); + mix-blend-mode: exclusion; + opacity: 0.8; + } + } } } } @@ -548,6 +584,18 @@ defineExpose({ width: auto; height: auto; min-width: 0; + + &:disabled { + cursor: not-allowed; + background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + opacity: 1; + + > .emoji { + filter: grayscale(1); + mix-blend-mode: exclusion; + opacity: 0.8; + } + } } } } @@ -663,6 +711,18 @@ defineExpose({ box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); } + &:disabled { + cursor: not-allowed; + background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%); + opacity: 1; + + > .emoji { + filter: grayscale(1); + mix-blend-mode: exclusion; + opacity: 0.8; + } + } + > .emoji { height: 1.25em; vertical-align: -.25em; diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 0dcd8b0ea2..bccee5109d 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -33,7 +33,8 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as sound from '@/scripts/sound.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; -import { customEmojis } from '@/custom-emojis.js'; +import { customEmojisMap } from '@/custom-emojis.js'; +import { unicodeEmojisMap } from '@/scripts/emojilist.js'; const props = defineProps<{ reaction: string; @@ -50,13 +51,11 @@ const emit = defineEmits<{ const buttonEl = shallowRef(); -const isCustomEmoji = computed(() => props.reaction.includes(':')); -const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null); +const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, '')); +const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? unicodeEmojisMap.get(props.reaction)); const canToggle = computed(() => { - return !props.reaction.match(/@\w/) && $i - && (emoji.value && checkReactionPermissions($i, props.note, emoji.value)) - || !isCustomEmoji.value; + return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value); }); const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts index c9d2a5bfc6..da704717c1 100644 --- a/packages/frontend/src/scripts/check-reaction-permissions.ts +++ b/packages/frontend/src/scripts/check-reaction-permissions.ts @@ -1,6 +1,10 @@ import * as Misskey from 'misskey-js'; +import { UnicodeEmojiDef } from './emojilist.js'; -export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean { +export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef): boolean { + if ('char' in emoji) return true; // UnicodeEmojiDefなら常にリアクション可能 + + emoji = emoji as Misskey.entities.EmojiSimple; const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? []; return !(emoji.localOnly && note.user.host !== me.host) && !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote')) diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts index 54d45e025f..2a6120f3ba 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend/src/scripts/emojilist.ts @@ -20,6 +20,10 @@ export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ category: unicodeEmojiCategories[x[2]], })); +export const unicodeEmojisMap = new Map( + emojilist.map(x => [x.char, x]) +); + const _indexByChar = new Map(); const _charGroupByCategory = new Map(); for (let i = 0; i < emojilist.length; i++) { From 41747b6ee2b2679517ef1f9fb94f333d40673ac5 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 24 Feb 2024 11:50:10 +0900 Subject: [PATCH 24/78] refactor --- .../core/entities/NoteReactionEntityService.ts | 15 +++++++++++++++ .../src/server/api/endpoints/users/reactions.ts | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts index 2799f58992..3f4fa3cf96 100644 --- a/packages/backend/src/core/entities/NoteReactionEntityService.ts +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -69,4 +69,19 @@ export class NoteReactionEntityService implements OnModuleInit { } : {}), }; } + + @bindThis + public async packMany( + reactions: MiNoteReaction[], + me?: { id: MiUser['id'] } | null | undefined, + options?: { + withNote: boolean; + }, + ): Promise[]> { + const opts = Object.assign({ + withNote: false, + }, options); + + return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts))); + } } diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index e20d896248..aca883a052 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -98,7 +98,7 @@ export default class extends Endpoint { // eslint- .limit(ps.limit) .getMany(); - return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me, { withNote: true }))); + return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true }); }); } } From 792168fdfacfd0bc316daadf1e32b953f69e1608 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Sat, 24 Feb 2024 18:06:10 +0900 Subject: [PATCH 25/78] =?UTF-8?q?fix(frontend):=20`userActivation`?= =?UTF-8?q?=E3=81=8C=E3=81=AA=E3=81=84=E7=92=B0=E5=A2=83=E3=81=AB=E3=81=8A?= =?UTF-8?q?=E3=81=84=E3=81=A6=E4=B8=8D=E5=85=B7=E5=90=88=E3=81=8C=E7=94=9F?= =?UTF-8?q?=E3=81=98=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20(#13451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/scripts/sound.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 67818320b3..fcd59510df 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -126,7 +126,7 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) */ export function playMisskeySfx(operationType: OperationType) { const sound = defaultStore.state[`sound_${operationType}`]; - if (sound.type == null || !canPlay || !navigator.userActivation.hasBeenActive) return; + if (sound.type == null || !canPlay || ('userActivation' in navigator && !navigator.userActivation.hasBeenActive)) return; canPlay = false; playMisskeySfxFile(sound).finally(() => { From 2c6f25b710b4f8095458fe88ddd56e6c6a41d006 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 25 Feb 2024 12:36:10 +0900 Subject: [PATCH 26/78] =?UTF-8?q?fix:=20=E5=8F=A4=E3=81=84=E3=82=AD?= =?UTF-8?q?=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=82=92=E4=BD=BF=E3=81=86?= =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#13453)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/core/AccountMoveService.ts | 4 +-- packages/backend/src/core/CacheService.ts | 4 ++- .../backend/src/core/GlobalEventService.ts | 1 + .../backend/src/core/UserFollowingService.ts | 29 +++++++------------ packages/backend/src/misc/cache.ts | 8 +++++ .../RelationshipProcessorService.ts | 2 +- .../server/api/endpoints/following/create.ts | 2 +- .../src/server/api/endpoints/i/update.ts | 6 ++-- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index b7796a5183..5bd885df40 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -20,7 +20,6 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { CacheService } from '@/core/CacheService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { MetaService } from '@/core/MetaService.js'; @@ -60,7 +59,6 @@ export class AccountMoveService { private instanceChart: InstanceChart, private metaService: MetaService, private relayService: RelayService, - private cacheService: CacheService, private queueService: QueueService, ) { } @@ -84,7 +82,7 @@ export class AccountMoveService { Object.assign(src, update); // Update cache - this.cacheService.uriPersonCache.set(srcUri, src); + this.globalEventService.publishInternalEvent('localUserUpdated', src); const srcPerson = await this.apRendererService.renderPerson(src); const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src)); diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 0fc47bf8ec..d008e7ec52 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -129,10 +129,12 @@ export class CacheService implements OnApplicationShutdown { switch (type) { case 'userChangeSuspendedState': case 'userChangeDeletedState': - case 'remoteUserUpdated': { + case 'remoteUserUpdated': + case 'localUserUpdated': { const user = await this.usersRepository.findOneBy({ id: body.id }); if (user == null) { this.userByIdCache.delete(body.id); + this.localUserByIdCache.delete(body.id); for (const [k, v] of this.uriPersonCache.cache.entries()) { if (v.value?.id === body.id) { this.uriPersonCache.delete(k); diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index a127a6df3b..7c1b34da05 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -212,6 +212,7 @@ export interface InternalEventTypes { userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; }; userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; }; remoteUserUpdated: { id: MiUser['id']; }; + localUserUpdated: { id: MiUser['id']; }; follow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index d87cbacdcb..0a492c06e4 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -101,33 +101,24 @@ export class UserFollowingService implements OnModuleInit { this.queueService.deliver(followee, content, follower.inbox, false); } - /** - * ThinUserでなくともユーザーの情報が最新でない場合はこちらを使うべき - */ - @bindThis - public async followByThinUser( - _follower: ThinUser, - _followee: ThinUser, - options: Parameters[2] = {}, - ) { - const [follower, followee] = await Promise.all([ - this.usersRepository.findOneByOrFail({ id: _follower.id }), - this.usersRepository.findOneByOrFail({ id: _followee.id }), - ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; - - await this.follow(follower, followee, options); - } - @bindThis public async follow( - follower: MiLocalUser | MiRemoteUser, - followee: MiLocalUser | MiRemoteUser, + _follower: ThinUser, + _followee: ThinUser, { requestId, silent = false, withReplies }: { requestId?: string, silent?: boolean, withReplies?: boolean, } = {}, ): Promise { + /** + * 必ず最新のユーザー情報を取得する + */ + const [follower, followee] = await Promise.all([ + this.usersRepository.findOneByOrFail({ id: _follower.id }), + this.usersRepository.findOneByOrFail({ id: _followee.id }), + ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isRemoteUser(followee)) { // What? throw new Error('Remote user cannot follow remote user.'); diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 7f4d1521b5..bba64a06ef 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -187,6 +187,10 @@ export class RedisSingleCache { // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? export class MemoryKVCache { + /** + * データを持つマップ + * @deprecated これを直接操作するべきではない + */ public cache: Map; private lifetime: number; private gcIntervalHandle: NodeJS.Timeout; @@ -201,6 +205,10 @@ export class MemoryKVCache { } @bindThis + /** + * Mapにキャッシュをセットします + * @deprecated これを直接呼び出すべきではない。InternalEventなどで変更を全てのプロセス/マシンに通知するべき + */ public set(key: string, value: T): void { this.cache.set(key, { date: Date.now(), diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts index 53dbb42169..408b02fb38 100644 --- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts +++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts @@ -35,7 +35,7 @@ export class RelationshipProcessorService { @bindThis public async processFollow(job: Bull.Job): Promise { this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? "with replies" : "without replies"}`); - await this.userFollowingService.followByThinUser(job.data.from, job.data.to, { + await this.userFollowingService.follow(job.data.from, job.data.to, { requestId: job.data.requestId, silent: job.data.silent, withReplies: job.data.withReplies, diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index 042d7f119d..db320e7129 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -71,7 +71,7 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id' }, - withReplies: { type: 'boolean' } + withReplies: { type: 'boolean' }, }, required: ['userId'], } as const; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index bf6c53d8eb..84a1931a3d 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -456,9 +456,9 @@ export default class extends Endpoint { // eslint- this.hashtagService.updateUsertags(user, tags); //#endregion - if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates); - if (Object.keys(updates).includes('alsoKnownAs')) { - this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates }); + if (Object.keys(updates).length > 0) { + await this.usersRepository.update(user.id, updates); + this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id }); } await this.userProfilesRepository.update(user.id, { From dd48366ed8130617df2563508369e3d4d63ed2a2 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sun, 25 Feb 2024 18:06:26 +0900 Subject: [PATCH 27/78] =?UTF-8?q?admin/emoji/update=E3=81=AE=E5=BF=85?= =?UTF-8?q?=E9=A0=88=E9=A0=85=E7=9B=AE=E3=82=92=E6=B8=9B=E3=82=89=E3=81=99?= =?UTF-8?q?=E3=80=80=E7=AD=89=20(#13449)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * admin/emoji/update enhancement * add CustomEmojiService.getEmojiByName * update endpoint * fix * Update update.ts * Update autogen files * type assertion * Update CHANGELOG.md --- CHANGELOG.md | 4 +++ .../backend/src/core/CustomEmojiService.ts | 5 ++++ .../api/endpoints/admin/emoji/update.ts | 27 ++++++++++++------- packages/misskey-js/src/autogen/types.ts | 6 ++--- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a939fa7621..ebbe22f2f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ - Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正 - エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました - Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正 +- エンドポイント`admin/emoji/update`の各種修正 + - 必須パラメータを`id`または`name`のいずれかのみに + - `id`の代わりに`name`で絵文字を指定可能に(`id`・`name`両指定時は従来通り`name`を変更する挙動) + - `category`および`licence`が指定なしの時勝手にnullに上書きされる挙動を修正 ## 2024.2.0 diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 64a8c1acdf..edb9335b6e 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -393,6 +393,11 @@ export class CustomEmojiService implements OnApplicationShutdown { return this.emojisRepository.findOneBy({ id }); } + @bindThis + public getEmojiByName(name: string): Promise { + return this.emojisRepository.findOneBy({ name, host: IsNull() }); + } + @bindThis public dispose(): void { this.cache.dispose(); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index a9ff4236d2..22609a16a3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -57,7 +57,10 @@ export const paramDef = { type: 'string', } }, }, - required: ['id', 'name', 'aliases'], + anyOf: [ + { required: ['id'] }, + { required: ['name'] }, + ], } as const; @Injectable() @@ -70,27 +73,33 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { let driveFile; - if (ps.fileId) { driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } - const emoji = await this.customEmojiService.getEmojiById(ps.id); - if (emoji != null) { - if (ps.name !== emoji.name) { + + let emojiId; + if (ps.id) { + emojiId = ps.id; + const emoji = await this.customEmojiService.getEmojiById(ps.id); + if (!emoji) throw new ApiError(meta.errors.noSuchEmoji); + if (ps.name && (ps.name !== emoji.name)) { const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); } } else { - throw new ApiError(meta.errors.noSuchEmoji); + if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.'); + const emoji = await this.customEmojiService.getEmojiByName(ps.name); + if (!emoji) throw new ApiError(meta.errors.noSuchEmoji); + emojiId = emoji.id; } - await this.customEmojiService.update(ps.id, { + await this.customEmojiService.update(emojiId, { driveFile, name: ps.name, - category: ps.category ?? null, + category: ps.category, aliases: ps.aliases, - license: ps.license ?? null, + license: ps.license, isSensitive: ps.isSensitive, localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 18bc45b983..07edf19c94 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -7089,13 +7089,13 @@ export type operations = { content: { 'application/json': { /** Format: misskey:id */ - id: string; - name: string; + id?: string; + name?: string; /** Format: misskey:id */ fileId?: string; /** @description Use `null` to reset the category. */ category?: string | null; - aliases: string[]; + aliases?: string[]; license?: string | null; isSensitive?: boolean; localOnly?: boolean; From 0a0af6887a829a45d2982bc61c319e76445f66a9 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Sun, 25 Feb 2024 18:06:40 +0900 Subject: [PATCH 28/78] =?UTF-8?q?test(frontend):=20Chromatic=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=8C=E8=90=BD=E3=81=A1=E3=82=8B=E3=81=AE?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=20(#13448)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(frontend): Chromaticテストが落ちるのを修正 * fix: テストケースを修正 * refactor: comment --- packages/frontend/.storybook/generate.tsx | 3 ++- packages/frontend/src/components/global/MkA.stories.impl.ts | 4 +++- .../frontend/src/components/global/MkTime.stories.impl.ts | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 76c5b6be4b..1e925aede6 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -401,7 +401,8 @@ function toStories(component: string): Promise { // glob('src/{components,pages,ui,widgets}/**/*.vue') (async () => { const globs = await Promise.all([ - glob('src/components/global/*.vue'), + glob('src/components/global/Mk*.vue'), + glob('src/components/global/RouterView.vue'), glob('src/components/Mk{A,B}*.vue'), glob('src/components/MkDigitalClock.vue'), glob('src/components/MkGalleryPostPreview.vue'), diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts index 9d57841f04..c1d8cf0ca6 100644 --- a/packages/frontend/src/components/global/MkA.stories.impl.ts +++ b/packages/frontend/src/components/global/MkA.stories.impl.ts @@ -32,7 +32,8 @@ export const Default = { async play({ canvasElement }) { const canvas = within(canvasElement); const a = canvas.getByRole('link'); - await expect(a.href).toMatch(/^https?:\/\/.*#test$/); + // FIXME: 通るけどその後落ちるのでコメントアウト + // await expect(a.href).toMatch(/^https?:\/\/.*#test$/); await userEvent.pointer({ keys: '[MouseRight]', target: a }); await tick(); const menu = canvas.getByRole('menu'); @@ -44,6 +45,7 @@ export const Default = { }, args: { to: '#test', + behavior: 'browser', }, parameters: { layout: 'centered', diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts index 8ddf8e213a..355c839113 100644 --- a/packages/frontend/src/components/global/MkTime.stories.impl.ts +++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts @@ -10,7 +10,7 @@ import MkTime from './MkTime.vue'; import { i18n } from '@/i18n.js'; import { dateTimeFormat } from '@/scripts/intl-const.js'; const now = new Date('2023-04-01T00:00:00.000Z'); -const future = new Date('3000-04-01T00:00:00.000Z'); +const future = new Date('2024-04-01T00:00:00.000Z'); const oneHourAgo = new Date(now.getTime() - 3600000); const oneDayAgo = new Date(now.getTime() - 86400000); const oneWeekAgo = new Date(now.getTime() - 604800000); @@ -49,7 +49,7 @@ export const Empty = { export const RelativeFuture = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.tsx._timeIn.years({ n: 977 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._timeIn.years({ n: 1 })); // n (1) = future (2024) - now (2023) }, args: { ...Empty.args, From 0fb7b98f96d809de10d5ff12ad57560c1fd7e1f1 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Mon, 26 Feb 2024 19:49:12 +0900 Subject: [PATCH 29/78] fix(backend): fix incorrect schemas (#13458) --- packages/backend/src/models/json-schema/user.ts | 3 +++ .../src/server/api/endpoints/admin/emoji/add.ts | 5 ++++- packages/misskey-js/etc/misskey-js.api.md | 4 ++++ packages/misskey-js/src/autogen/endpoint.ts | 3 ++- packages/misskey-js/src/autogen/entities.ts | 1 + packages/misskey-js/src/autogen/types.ts | 12 ++++++++---- 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index c7f86635da..952cd6bf80 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -148,6 +148,9 @@ export const packedUserLiteSchema = { emojis: { type: 'object', nullable: false, optional: false, + additionalProperties: { + type: 'string', + }, }, onlineStatus: { type: 'string', diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index e32a19120d..796f273330 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -31,7 +31,10 @@ export const meta = { }, }, - ref: 'EmojiDetailed', + res: { + type: 'object', + ref: 'EmojiDetailed', + }, } as const; export const paramDef = { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index a2d5a4f514..b5e7ec7548 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -124,6 +124,9 @@ type AdminEmojiAddAliasesBulkRequest = operations['admin/emoji/add-aliases-bulk' // @public (undocumented) type AdminEmojiAddRequest = operations['admin/emoji/add']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminEmojiAddResponse = operations['admin/emoji/add']['responses']['200']['content']['application/json']; + // @public (undocumented) type AdminEmojiCopyRequest = operations['admin/emoji/copy']['requestBody']['content']['application/json']; @@ -1154,6 +1157,7 @@ declare namespace entities { AdminDriveShowFileResponse, AdminEmojiAddAliasesBulkRequest, AdminEmojiAddRequest, + AdminEmojiAddResponse, AdminEmojiCopyRequest, AdminEmojiCopyResponse, AdminEmojiDeleteBulkRequest, diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 595d0d66c0..656ac28246 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -35,6 +35,7 @@ import type { AdminDriveShowFileResponse, AdminEmojiAddAliasesBulkRequest, AdminEmojiAddRequest, + AdminEmojiAddResponse, AdminEmojiCopyRequest, AdminEmojiCopyResponse, AdminEmojiDeleteBulkRequest, @@ -578,7 +579,7 @@ export type Endpoints = { 'admin/drive/files': { req: AdminDriveFilesRequest; res: AdminDriveFilesResponse }; 'admin/drive/show-file': { req: AdminDriveShowFileRequest; res: AdminDriveShowFileResponse }; 'admin/emoji/add-aliases-bulk': { req: AdminEmojiAddAliasesBulkRequest; res: EmptyResponse }; - 'admin/emoji/add': { req: AdminEmojiAddRequest; res: EmptyResponse }; + 'admin/emoji/add': { req: AdminEmojiAddRequest; res: AdminEmojiAddResponse }; 'admin/emoji/copy': { req: AdminEmojiCopyRequest; res: AdminEmojiCopyResponse }; 'admin/emoji/delete-bulk': { req: AdminEmojiDeleteBulkRequest; res: EmptyResponse }; 'admin/emoji/delete': { req: AdminEmojiDeleteRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index e7ed146c43..a936931e99 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -37,6 +37,7 @@ export type AdminDriveShowFileRequest = operations['admin/drive/show-file']['req export type AdminDriveShowFileResponse = operations['admin/drive/show-file']['responses']['200']['content']['application/json']; export type AdminEmojiAddAliasesBulkRequest = operations['admin/emoji/add-aliases-bulk']['requestBody']['content']['application/json']; export type AdminEmojiAddRequest = operations['admin/emoji/add']['requestBody']['content']['application/json']; +export type AdminEmojiAddResponse = operations['admin/emoji/add']['responses']['200']['content']['application/json']; export type AdminEmojiCopyRequest = operations['admin/emoji/copy']['requestBody']['content']['application/json']; export type AdminEmojiCopyResponse = operations['admin/emoji/copy']['responses']['200']['content']['application/json']; export type AdminEmojiDeleteBulkRequest = operations['admin/emoji/delete-bulk']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 07edf19c94..733670d704 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3588,7 +3588,9 @@ export type components = { faviconUrl: string | null; themeColor: string | null; }; - emojis: Record; + emojis: { + [key: string]: string; + }; /** @enum {string} */ onlineStatus: 'unknown' | 'online' | 'active' | 'offline'; badgeRoles?: ({ @@ -6476,9 +6478,11 @@ export type operations = { }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['EmojiDetailed']; + }; }; /** @description Client error */ 400: { From f906ad6ca7044e4c509a5fe01f398f841a44027a Mon Sep 17 00:00:00 2001 From: zawa-ch Date: Tue, 27 Feb 2024 18:45:46 +0900 Subject: [PATCH 30/78] =?UTF-8?q?Enhance:=20=E3=82=B3=E3=83=B3=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=82=B7=E3=83=A7=E3=83=8A=E3=83=AB=E3=83=AD=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E6=9D=A1=E4=BB=B6=E3=81=AB=E3=80=8C=E3=83=9E?= =?UTF-8?q?=E3=83=8B=E3=83=A5=E3=82=A2=E3=83=AB=E3=83=AD=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=81=B8=E3=81=AE=E3=82=A2=E3=82=B5=E3=82=A4=E3=83=B3=E3=80=8D?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=20(#13463)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 * コメント修正 --- CHANGELOG.md | 1 + locales/index.d.ts | 4 +++ locales/ja-JP.yml | 1 + packages/backend/src/core/RoleService.ts | 19 +++++++------ packages/backend/src/misc/json-schema.ts | 2 ++ packages/backend/src/models/Role.ts | 6 ++++ .../backend/src/models/json-schema/role.ts | 20 +++++++++++++ packages/backend/test/unit/RoleService.ts | 28 +++++++++++++++++++ .../src/pages/admin/RolesEditorFormula.vue | 9 ++++++ packages/misskey-js/etc/misskey-js.api.md | 4 +++ packages/misskey-js/src/autogen/models.ts | 1 + packages/misskey-js/src/autogen/types.ts | 11 +++++++- 12 files changed, 97 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebbe22f2f6..513338e667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### General - Enhance: サーバーごとにモデレーションノートを残せるように +- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 ### Client - Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 diff --git a/locales/index.d.ts b/locales/index.d.ts index 1a2565b06a..7d5f8ce732 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6528,6 +6528,10 @@ export interface Locale extends ILocale { "avatarDecorationLimit": string; }; "_condition": { + /** + * マニュアルロールにアサイン済み + */ + "roleAssignedTo": string; /** * ローカルユーザー */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 61c61b8f96..1bb56738c6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1687,6 +1687,7 @@ _role: canUseTranslator: "翻訳機能の利用" avatarDecorationLimit: "アイコンデコレーションの最大取付個数" _condition: + roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" isRemote: "リモートユーザー" createdLessThan: "アカウント作成から~以内" diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index c5baaf3fff..8312489a78 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -200,17 +200,20 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private evalCond(user: MiUser, value: RoleCondFormulaValue): boolean { + private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean { try { switch (value.type) { case 'and': { - return value.values.every(v => this.evalCond(user, v)); + return value.values.every(v => this.evalCond(user, roles, v)); } case 'or': { - return value.values.some(v => this.evalCond(user, v)); + return value.values.some(v => this.evalCond(user, roles, v)); } case 'not': { - return !this.evalCond(user, value.value); + return !this.evalCond(user, roles, value.value); + } + case 'roleAssignedTo': { + return roles.some(r => r.id === value.roleId); } case 'isLocal': { return this.userEntityService.isLocalUser(user); @@ -272,7 +275,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { const assigns = await this.getUserAssigns(userId); const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id)); const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; - const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); + const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula)); return [...assignedRoles, ...matchedCondRoles]; } @@ -285,13 +288,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); // 期限切れのロールを除外 assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); - const assignedRoleIds = assigns.map(x => x.roleId); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); - const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); + const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id)); + const assignedBadgeRoles = assignedRoles.filter(r => r.asBadge); const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional')); if (badgeCondRoles.length > 0) { const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; - const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula)); + const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula)); return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; } else { return assignedBadgeRoles; diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 8449e5ff07..46b0bb2fab 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -44,6 +44,7 @@ import { packedRoleCondFormulaLogicsSchema, packedRoleCondFormulaValueNot, packedRoleCondFormulaValueIsLocalOrRemoteSchema, + packedRoleCondFormulaValueAssignedRoleSchema, packedRoleCondFormulaValueCreatedSchema, packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, packedRoleCondFormulaValueSchema, @@ -96,6 +97,7 @@ export const refs = { RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema, RoleCondFormulaValueNot: packedRoleCondFormulaValueNot, RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema, + RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema, RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema, RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, RoleCondFormulaValue: packedRoleCondFormulaValueSchema, diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/Role.ts index fa05ea8637..058abe3118 100644 --- a/packages/backend/src/models/Role.ts +++ b/packages/backend/src/models/Role.ts @@ -29,6 +29,11 @@ type CondFormulaValueIsRemote = { type: 'isRemote'; }; +type CondFormulaValueRoleAssignedTo = { + type: 'roleAssignedTo'; + roleId: string; +}; + type CondFormulaValueCreatedLessThan = { type: 'createdLessThan'; sec: number; @@ -75,6 +80,7 @@ export type RoleCondFormulaValue = { id: string } & ( CondFormulaValueNot | CondFormulaValueIsLocal | CondFormulaValueIsRemote | + CondFormulaValueRoleAssignedTo | CondFormulaValueCreatedLessThan | CondFormulaValueCreatedMoreThan | CondFormulaValueFollowersLessThanOrEq | diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index ef6b279bee..9f2b5b17ed 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -57,6 +57,23 @@ export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = { }, } as const; +export const packedRoleCondFormulaValueAssignedRoleSchema = { + type: 'object', + properties: { + type: { + type: 'string', + nullable: false, optional: false, + enum: ['roleAssignedTo'], + }, + roleId: { + type: 'string', + nullable: false, optional: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + }, +} as const; + export const packedRoleCondFormulaValueCreatedSchema = { type: 'object', properties: { @@ -115,6 +132,9 @@ export const packedRoleCondFormulaValueSchema = { { ref: 'RoleCondFormulaValueIsLocalOrRemote', }, + { + ref: 'RoleCondFormulaValueAssignedRole', + }, { ref: 'RoleCondFormulaValueCreated', }, diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 5222745b7f..fe5ad31597 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -251,6 +251,34 @@ describe('RoleService', () => { expect(user2Policies.canManageCustomEmojis).toBe(true); }); + test('コンディショナルロール: マニュアルロールにアサイン済み', async () => { + const [user1, user2, role1] = await Promise.all([ + createUser(), + createUser(), + createRole({ + name: 'manual role', + }), + ]); + const role2 = await createRole({ + name: 'conditional role', + target: 'conditional', + condFormula: { + // idはバックエンドのロジックに必要ない? + id: 'bdc612bd-9d54-4675-ae83-0499c82ea670', + type: 'roleAssignedTo', + roleId: role1.id, + }, + }); + await roleService.assign(user2.id, role1.id); + + const [u1role, u2role] = await Promise.all([ + roleService.getUserRoles(user1.id), + roleService.getUserRoles(user2.id), + ]); + expect(u1role.some(r => r.id === role2.id)).toBe(false); + expect(u2role.some(r => r.id === role2.id)).toBe(true); + }); + test('expired role', async () => { const user = await createUser(); const role = await createRole({ diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue index f4a8f44955..2f5b4c47d8 100644 --- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue +++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue @@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -51,6 +52,10 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
@@ -62,6 +67,7 @@ import MkSelect from '@/components/MkSelect.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { deepClone } from '@/scripts/clone.js'; +import { rolesCache } from '@/cache.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -77,6 +83,8 @@ const props = defineProps<{ const v = ref(deepClone(props.modelValue)); +const roles = await rolesCache.fetch(); + watch(() => props.modelValue, () => { if (JSON.stringify(props.modelValue) === JSON.stringify(v.value)) return; v.value = deepClone(props.modelValue); @@ -92,6 +100,7 @@ const type = computed({ if (t === 'and') v.value.values = []; if (t === 'or') v.value.values = []; if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' }; + if (t === 'roleAssignedTo') v.value.roleId = ''; if (t === 'createdLessThan') v.value.sec = 86400; if (t === 'createdMoreThan') v.value.sec = 86400; if (t === 'followersLessThanOrEq') v.value.value = 10; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index b5e7ec7548..0e990ffd5a 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1712,6 +1712,7 @@ declare namespace entities { RoleCondFormulaLogics, RoleCondFormulaValueNot, RoleCondFormulaValueIsLocalOrRemote, + RoleCondFormulaValueAssignedRole, RoleCondFormulaValueCreated, RoleCondFormulaFollowersOrFollowingOrNotes, RoleCondFormulaValue, @@ -2731,6 +2732,9 @@ type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics']; // @public (undocumented) type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue']; +// @public (undocumented) +type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole']; + // @public (undocumented) type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index ab49f9478a..6f61458600 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -38,6 +38,7 @@ export type Signin = components['schemas']['Signin']; export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics']; export type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot']; export type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote']; +export type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole']; export type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated']; export type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; export type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 733670d704..8d700fb828 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4573,6 +4573,15 @@ export type components = { /** @enum {string} */ type: 'isLocal' | 'isRemote'; }; + RoleCondFormulaValueAssignedRole: { + /** @enum {string} */ + type: 'roleAssignedTo'; + /** + * Format: id + * @example xxxxxxxxxx + */ + roleId: string; + }; RoleCondFormulaValueCreated: { id: string; /** @enum {string} */ @@ -4585,7 +4594,7 @@ export type components = { type: 'followersLessThanOrEq' | 'followersMoreThanOrEq' | 'followingLessThanOrEq' | 'followingMoreThanOrEq' | 'notesLessThanOrEq' | 'notesMoreThanOrEq'; value: number; }; - RoleCondFormulaValue: components['schemas']['RoleCondFormulaLogics'] | components['schemas']['RoleCondFormulaValueNot'] | components['schemas']['RoleCondFormulaValueIsLocalOrRemote'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; + RoleCondFormulaValue: components['schemas']['RoleCondFormulaLogics'] | components['schemas']['RoleCondFormulaValueNot'] | components['schemas']['RoleCondFormulaValueIsLocalOrRemote'] | components['schemas']['RoleCondFormulaValueAssignedRole'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; RoleLite: { /** * Format: id From 0d47877db1e1012aaba78a2926b165cf9e039d3d Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Wed, 28 Feb 2024 09:49:34 +0900 Subject: [PATCH 31/78] =?UTF-8?q?enhance(backend):=20=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=BB=E3=83=95=E3=82=A9=E3=83=AD=E3=83=AF?= =?UTF-8?q?=E3=83=BC=E9=96=A2=E9=80=A3=E3=81=AE=E9=80=9A=E7=9F=A5=E3=81=AE?= =?UTF-8?q?=E5=8F=97=E4=BF=A1=E8=A8=AD=E5=AE=9A=E3=81=AE=E5=BC=B7=E5=8C=96?= =?UTF-8?q?=20(#13468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(backend): 通知の受信設定に「フォロー中またはフォロワー」を追加 * fix(backend): 通知の受信設定で「相互フォロー」が正しく動作しない問題を修正 * Update CHANGELOG.md --- CHANGELOG.md | 2 + locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + .../backend/src/core/NotificationService.ts | 8 ++ packages/backend/src/models/UserProfile.ts | 2 + .../backend/src/models/json-schema/user.ts | 2 +- .../notifications.notification-config.vue | 1 + .../src/pages/settings/notifications.vue | 1 + packages/misskey-js/src/autogen/types.ts | 84 +++++++++---------- 9 files changed, 62 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 513338e667..010d5aed7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### General - Enhance: サーバーごとにモデレーションノートを残せるように - Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 +- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加 ### Client - Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 @@ -33,6 +34,7 @@ - 必須パラメータを`id`または`name`のいずれかのみに - `id`の代わりに`name`で絵文字を指定可能に(`id`・`name`両指定時は従来通り`name`を変更する挙動) - `category`および`licence`が指定なしの時勝手にnullに上書きされる挙動を修正 +- Fix: 通知の受信設定で「相互フォロー」が正しく動作しない問題を修正 ## 2024.2.0 diff --git a/locales/index.d.ts b/locales/index.d.ts index 7d5f8ce732..3edc9d235e 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4656,6 +4656,10 @@ export interface Locale extends ILocale { * 相互フォロー */ "mutualFollow": string; + /** + * フォロー中またはフォロワー + */ + "followingOrFollower": string; /** * ファイル付きのみ */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1bb56738c6..66ddf6a46d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1160,6 +1160,7 @@ showRenotes: "リノートを表示" edited: "編集済み" notificationRecieveConfig: "通知の受信設定" mutualFollow: "相互フォロー" +followingOrFollower: "フォロー中またはフォロワー" fileAttachedOnly: "ファイル付きのみ" showRepliesToOthersInTimeline: "TLに他の人への返信を含める" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index ee16193579..7224341991 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -122,6 +122,14 @@ export class NotificationService implements OnApplicationShutdown { return null; } } else if (recieveConfig?.type === 'mutualFollow') { + const [isFollowing, isFollower] = await Promise.all([ + this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), + this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), + ]); + if (!(isFollowing && isFollower)) { + return null; + } + } else if (recieveConfig?.type === 'followingOrFollower') { const [isFollowing, isFollower] = await Promise.all([ this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 1ca2f55850..7dbe0b3717 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -249,6 +249,8 @@ export class MiUserProfile { type: 'follower'; } | { type: 'mutualFollow'; + } | { + type: 'followingOrFollower'; } | { type: 'list'; userListId: MiUserList['id']; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 952cd6bf80..947a9317d7 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -13,7 +13,7 @@ export const notificationRecieveConfig = { type: { type: 'string', nullable: false, - enum: ['all', 'following', 'follower', 'mutualFollow', 'never'], + enum: ['all', 'following', 'follower', 'mutualFollow', 'followingOrFollower', 'never'], }, }, required: ['type'], diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue index d6aac63674..a36f036303 100644 --- a/packages/frontend/src/pages/settings/notifications.notification-config.vue +++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only + diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index febcfa32ed..bbcef65283 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only $i.notificationRecieveConfig[type]?.type === 'following' ? i18n.ts.following : $i.notificationRecieveConfig[type]?.type === 'follower' ? i18n.ts.followers : $i.notificationRecieveConfig[type]?.type === 'mutualFollow' ? i18n.ts.mutualFollow : + $i.notificationRecieveConfig[type]?.type === 'followingOrFollower' ? i18n.ts.followingOrFollower : $i.notificationRecieveConfig[type]?.type === 'list' ? i18n.ts.userList : i18n.ts.all }} diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 8d700fb828..a3597e4635 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3700,7 +3700,7 @@ export type components = { notificationRecieveConfig: { note?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3709,7 +3709,7 @@ export type components = { }]>; follow?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3718,7 +3718,7 @@ export type components = { }]>; mention?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3727,7 +3727,7 @@ export type components = { }]>; reply?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3736,7 +3736,7 @@ export type components = { }]>; renote?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3745,7 +3745,7 @@ export type components = { }]>; quote?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3754,7 +3754,7 @@ export type components = { }]>; reaction?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3763,7 +3763,7 @@ export type components = { }]>; pollEnded?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3772,7 +3772,7 @@ export type components = { }]>; receiveFollowRequest?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3781,7 +3781,7 @@ export type components = { }]>; followRequestAccepted?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3790,7 +3790,7 @@ export type components = { }]>; roleAssigned?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3799,7 +3799,7 @@ export type components = { }]>; achievementEarned?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3808,7 +3808,7 @@ export type components = { }]>; app?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -3817,7 +3817,7 @@ export type components = { }]>; test?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8436,7 +8436,7 @@ export type operations = { notificationRecieveConfig: { note?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8445,7 +8445,7 @@ export type operations = { }]>; follow?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8454,7 +8454,7 @@ export type operations = { }]>; mention?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8463,7 +8463,7 @@ export type operations = { }]>; reply?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8472,7 +8472,7 @@ export type operations = { }]>; renote?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8481,7 +8481,7 @@ export type operations = { }]>; quote?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8490,7 +8490,7 @@ export type operations = { }]>; reaction?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8499,7 +8499,7 @@ export type operations = { }]>; pollEnded?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8508,7 +8508,7 @@ export type operations = { }]>; receiveFollowRequest?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8517,7 +8517,7 @@ export type operations = { }]>; followRequestAccepted?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8526,7 +8526,7 @@ export type operations = { }]>; roleAssigned?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8535,7 +8535,7 @@ export type operations = { }]>; achievementEarned?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8544,7 +8544,7 @@ export type operations = { }]>; app?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -8553,7 +8553,7 @@ export type operations = { }]>; test?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18787,7 +18787,7 @@ export type operations = { notificationRecieveConfig?: { note?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18796,7 +18796,7 @@ export type operations = { }]>; follow?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18805,7 +18805,7 @@ export type operations = { }]>; mention?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18814,7 +18814,7 @@ export type operations = { }]>; reply?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18823,7 +18823,7 @@ export type operations = { }]>; renote?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18832,7 +18832,7 @@ export type operations = { }]>; quote?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18841,7 +18841,7 @@ export type operations = { }]>; reaction?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18850,7 +18850,7 @@ export type operations = { }]>; pollEnded?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18859,7 +18859,7 @@ export type operations = { }]>; receiveFollowRequest?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18868,7 +18868,7 @@ export type operations = { }]>; followRequestAccepted?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18877,7 +18877,7 @@ export type operations = { }]>; roleAssigned?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18886,7 +18886,7 @@ export type operations = { }]>; achievementEarned?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18895,7 +18895,7 @@ export type operations = { }]>; app?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; @@ -18904,7 +18904,7 @@ export type operations = { }]>; test?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; }, { /** @enum {string} */ type: 'list'; From b7d9d1620161a728e34ab20d8ea99160b3eb4196 Mon Sep 17 00:00:00 2001 From: okayurisotto <47853651+okayurisotto@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:34:58 +0900 Subject: [PATCH 32/78] =?UTF-8?q?refactor(backend):=20=E3=83=8E=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=AE=E3=82=A8=E3=82=AF=E3=82=B9=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=88=E5=87=A6=E7=90=86=E3=81=A7Streams=20API=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E3=81=86=E3=82=88=E3=81=86=E3=81=AB=20(#13465)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(backend): ノートのエクスポート処理でStreams APIを使うように * fixup! refactor(backend): ノートのエクスポート処理でStreams APIを使うように `await`忘れにより、ジョブがすぐに完了したことになり削除されてしまっていた。 それによって、`NoteStream`内での`updateProgress`メソッドの呼び出しで、`Missing key for job`のエラーが発生することがあった。 --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- packages/backend/src/misc/FileWriterStream.ts | 31 ++++ packages/backend/src/misc/JsonArrayStream.ts | 30 ++++ .../processors/ExportNotesProcessorService.ts | 164 +++++++++--------- 3 files changed, 146 insertions(+), 79 deletions(-) create mode 100644 packages/backend/src/misc/FileWriterStream.ts create mode 100644 packages/backend/src/misc/JsonArrayStream.ts diff --git a/packages/backend/src/misc/FileWriterStream.ts b/packages/backend/src/misc/FileWriterStream.ts new file mode 100644 index 0000000000..828851df0e --- /dev/null +++ b/packages/backend/src/misc/FileWriterStream.ts @@ -0,0 +1,31 @@ +import * as fs from 'node:fs/promises'; +import type { PathLike } from 'node:fs'; + +/** + * `fs.createWriteStream()`相当のことを行う`WritableStream` (Web標準) + */ +export class FileWriterStream extends WritableStream { + constructor(path: PathLike) { + let file: fs.FileHandle | null = null; + + super({ + start: async () => { + file = await fs.open(path, 'a'); + }, + write: async (chunk, controller) => { + if (file === null) { + controller.error(); + throw new Error(); + } + + await file.write(chunk); + }, + close: async () => { + await file?.close(); + }, + abort: async () => { + await file?.close(); + }, + }); + } +} diff --git a/packages/backend/src/misc/JsonArrayStream.ts b/packages/backend/src/misc/JsonArrayStream.ts new file mode 100644 index 0000000000..ad35bb3a79 --- /dev/null +++ b/packages/backend/src/misc/JsonArrayStream.ts @@ -0,0 +1,30 @@ +import { TransformStream } from 'node:stream/web'; + +/** + * ストリームに流れてきた各データについて`JSON.stringify()`した上で、それらを一つの配列にまとめる + */ +export class JsonArrayStream extends TransformStream { + constructor() { + /** 最初の要素かどうかを変数に記録 */ + let isFirst = true; + + super({ + start(controller) { + controller.enqueue('['); + }, + flush(controller) { + controller.enqueue(']'); + }, + transform(chunk, controller) { + if (isFirst) { + isFirst = false; + } else { + // 妥当なJSON配列にするためには最初以外の要素の前に`,`を挿入しなければならない + controller.enqueue(',\n'); + } + + controller.enqueue(JSON.stringify(chunk)); + }, + }); + } +} diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index f2ae0ce4b4..c7611012d7 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as fs from 'node:fs'; +import { ReadableStream, TextEncoderStream } from 'node:stream/web'; import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; @@ -18,10 +18,82 @@ import { bindThis } from '@/decorators.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { Packed } from '@/misc/json-schema.js'; import { IdService } from '@/core/IdService.js'; +import { JsonArrayStream } from '@/misc/JsonArrayStream.js'; +import { FileWriterStream } from '@/misc/FileWriterStream.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; +class NoteStream extends ReadableStream> { + constructor( + job: Bull.Job, + notesRepository: NotesRepository, + pollsRepository: PollsRepository, + driveFileEntityService: DriveFileEntityService, + idService: IdService, + userId: string, + ) { + let exportedNotesCount = 0; + let cursor: MiNote['id'] | null = null; + + const serialize = ( + note: MiNote, + poll: MiPoll | null, + files: Packed<'DriveFile'>[], + ): Record => { + return { + id: note.id, + text: note.text, + createdAt: idService.parse(note.id).date.toISOString(), + fileIds: note.fileIds, + files: files, + replyId: note.replyId, + renoteId: note.renoteId, + poll: poll, + cw: note.cw, + visibility: note.visibility, + visibleUserIds: note.visibleUserIds, + localOnly: note.localOnly, + reactionAcceptance: note.reactionAcceptance, + }; + }; + + super({ + async pull(controller): Promise { + const notes = await notesRepository.find({ + where: { + userId, + ...(cursor !== null ? { id: MoreThan(cursor) } : {}), + }, + take: 100, // 100件ずつ取得 + order: { id: 1 }, + }); + + if (notes.length === 0) { + job.updateProgress(100); + controller.close(); + } + + cursor = notes.at(-1)?.id ?? null; + + for (const note of notes) { + const poll = note.hasPoll + ? await pollsRepository.findOneByOrFail({ noteId: note.id }) // N+1 + : null; + const files = await driveFileEntityService.packManyByIds(note.fileIds); // N+1 + const content = serialize(note, poll, files); + + controller.enqueue(content); + exportedNotesCount++; + } + + const total = await notesRepository.countBy({ userId }); + job.updateProgress(exportedNotesCount / total); + }, + }); + } +} + @Injectable() export class ExportNotesProcessorService { private logger: Logger; @@ -59,67 +131,19 @@ export class ExportNotesProcessorService { this.logger.info(`Temp file is ${path}`); try { - const stream = fs.createWriteStream(path, { flags: 'a' }); + // メモリが足りなくならないようにストリームで処理する + await new NoteStream( + job, + this.notesRepository, + this.pollsRepository, + this.driveFileEntityService, + this.idService, + user.id, + ) + .pipeThrough(new JsonArrayStream()) + .pipeThrough(new TextEncoderStream()) + .pipeTo(new FileWriterStream(path)); - const write = (text: string): Promise => { - return new Promise((res, rej) => { - stream.write(text, err => { - if (err) { - this.logger.error(err); - rej(err); - } else { - res(); - } - }); - }); - }; - - await write('['); - - let exportedNotesCount = 0; - let cursor: MiNote['id'] | null = null; - - while (true) { - const notes = await this.notesRepository.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }) as MiNote[]; - - if (notes.length === 0) { - job.updateProgress(100); - break; - } - - cursor = notes.at(-1)?.id ?? null; - - for (const note of notes) { - let poll: MiPoll | undefined; - if (note.hasPoll) { - poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); - } - const files = await this.driveFileEntityService.packManyByIds(note.fileIds); - const content = JSON.stringify(this.serialize(note, poll, files)); - const isFirst = exportedNotesCount === 0; - await write(isFirst ? content : ',\n' + content); - exportedNotesCount++; - } - - const total = await this.notesRepository.countBy({ - userId: user.id, - }); - - job.updateProgress(exportedNotesCount / total); - } - - await write(']'); - - stream.end(); this.logger.succ(`Exported to: ${path}`); const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; @@ -130,22 +154,4 @@ export class ExportNotesProcessorService { cleanup(); } } - - private serialize(note: MiNote, poll: MiPoll | null = null, files: Packed<'DriveFile'>[]): Record { - return { - id: note.id, - text: note.text, - createdAt: this.idService.parse(note.id).date.toISOString(), - fileIds: note.fileIds, - files: files, - replyId: note.replyId, - renoteId: note.renoteId, - poll: poll, - cw: note.cw, - visibility: note.visibility, - visibleUserIds: note.visibleUserIds, - localOnly: note.localOnly, - reactionAcceptance: note.reactionAcceptance, - }; - } } From 664aeb3ced65f3911c8a21c2d5ffbd1035aec31a Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:43:17 +0900 Subject: [PATCH 33/78] =?UTF-8?q?fix(backend):=20=E3=83=AA=E3=83=8E?= =?UTF-8?q?=E3=83=BC=E3=83=88=E6=99=82=E3=81=AEHTL=E3=81=B8=E3=81=AE?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=AA=E3=83=BC=E3=83=9F=E3=83=B3=E3=82=B0?= =?UTF-8?q?=E3=81=AE=E6=84=8F=E5=9B=B3=E3=81=97=E3=81=AA=E3=81=84=E6=8C=99?= =?UTF-8?q?=E5=8B=95=E3=82=92=E4=BF=AE=E6=AD=A3=20(#13425)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): リノート時のストリーミングの意図しない挙動を修正 * Update CHANGELOG.md * fix: 不要な返り値 * fix: 不適切な条件分岐を修正 * test(backend): add htl tests --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 2 + .../api/stream/channels/home-timeline.ts | 10 ++- packages/backend/test/e2e/streaming.ts | 65 +++++++++++++++++-- packages/backend/test/utils.ts | 6 +- 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 010d5aed7a..f52226a635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ - Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正 - エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました - Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正 +- Fix: 自分がフォローしていないアカウントのフォロワー限定ノートが閲覧できることがある問題を修正 +- Fix: タイムラインのオプションで「リノートを表示」を無効にしている際、投票のみの引用リノートが流れてこない問題を修正 - エンドポイント`admin/emoji/update`の各種修正 - 必須パラメータを`id`または`name`のいずれかのみに - `id`の代わりに`name`で絵文字を指定可能に(`id`・`name`両指定時は従来通り`name`を変更する挙動) 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 ce9d7f5647..f45bf8622e 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -71,7 +71,15 @@ class HomeTimelineChannel extends Channel { } } - if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 純粋なリノート(引用リノートでないリノート)の場合 + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && note.poll == null) { + 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)) return; + } + } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 071daa275f..13d5a683ba 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -40,9 +40,9 @@ describe('Streaming', () => { let chinatsu: misskey.entities.SignupResponse; let takumi: misskey.entities.SignupResponse; - let kyokoNote: any; - let kanakoNote: any; - let takumiNote: any; + let kyokoNote: misskey.entities.Note; + let kanakoNote: misskey.entities.Note; + let takumiNote: misskey.entities.Note; let list: any; beforeAll(async () => { @@ -68,6 +68,9 @@ describe('Streaming', () => { // Follow: ayano => akari await follow(ayano, akari); + // Follow: kyoko => chitose + await api('following/create', { userId: chitose.id }, kyoko); + // Mute: chitose => kanako await api('mute/create', { userId: kanako.id }, chitose); @@ -170,7 +173,28 @@ describe('Streaming', () => { */ test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => { - // TODO + const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' }); + + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'reply to chitose\'s followers-only post', replyId: chitoseNote.id }, kyoko), // kyoko's reply to chitose's followers-only post + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + + test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信のリノートが流れない', async () => { + const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' }); + const kyokoReply = await post(kyoko, { text: 'reply to followers-only post', replyId: chitoseNote.id }); + + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { renoteId: kyokoReply.id }, kyoko), // kyoko's renote of kyoko's reply to chitose's followers-only post + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); }); test('フォローしていないユーザーの投稿は流れない', async () => { @@ -202,6 +226,39 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); + + test('withRenotes: false のときリノートが流れない', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { renoteId: kyokoNote.id }, kyoko), // kyoko renote + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + { withRenotes: false }, + ); + + assert.strictEqual(fired, false); + }); + + test('withRenotes: false のとき引用リノートが流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'quote', renoteId: kyokoNote.id }, kyoko), // kyoko quote + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + { withRenotes: false }, + ); + + assert.strictEqual(fired, true); + }); + + test('withRenotes: false のとき投票のみのリノートが流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { poll: { choices: ['kinoko', 'takenoko'] }, renoteId: kyokoNote.id }, kyoko), // kyoko renote with poll + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + { withRenotes: false }, + ); + + assert.strictEqual(fired, true); + }); }); // Home describe('Local Timeline', () => { diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index a2220ffae6..cd5dddd68d 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -355,7 +355,7 @@ export const uploadUrl = async (user: UserToken, url: string): Promise) => any, params?: any): Promise { +export function connectStream(user: UserToken, channel: C, listener: (message: Record) => any, params?: misskey.Channels[C]['params']): Promise { return new Promise((res, rej) => { const url = new URL(`ws://127.0.0.1:${port}/streaming`); const options: ClientOptions = {}; @@ -390,7 +390,7 @@ export function connectStream(user: UserToken, channel: string, listener: (messa }); } -export const waitFire = async (user: UserToken, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { +export const waitFire = async (user: UserToken, channel: C, trgr: () => any, cond: (msg: Record) => boolean, params?: misskey.Channels[C]['params']) => { return new Promise(async (res, rej) => { let timer: NodeJS.Timeout | null = null; @@ -435,7 +435,7 @@ export const waitFire = async (user: UserToken, channel: string, trgr: () => any */ export function makeStreamCatcher( user: UserToken, - channel: string, + channel: keyof misskey.Channels, cond: (message: Record) => boolean, extractor: (message: Record) => T, timeout = 60 * 1000): Promise { From 29350c9f334f426567e71eed479ae60ab4dea690 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Wed, 28 Feb 2024 18:26:38 +0900 Subject: [PATCH 34/78] =?UTF-8?q?refactor(frontend):=20`os.ts`=E5=91=A8?= =?UTF-8?q?=E3=82=8A=E3=81=AE=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=B0=20(#13186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(frontend): `os.ts`周りのリファクタリング * refactor: apiWithDialogのdataの型付け * refactor: 不要なas anyを除去 * refactor: 返り値の型を明記、`selectDriveFolder`は`File`のほうに合わせるよう返り値を変更 * refactor: 返り値の型を改善 * refactor: フォームの型を改善 * refactor: 良い感じのimportに修正 * refactor: フォームの返り値の型を改善 * refactor: `popup()`の`props`に`ref`な値を入れるのを許可するように * fix: `os.input`系と`os.select`の返り値の型がおかしい問題とそれによるバグを修正 * Update CHANGELOG.md * Update CHANGELOG.md --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 2 + packages/frontend/src/account.ts | 2 +- packages/frontend/src/components/MkDialog.vue | 29 +- .../src/components/MkDriveSelectDialog.vue | 8 +- .../src/components/MkEmojiPickerDialog.vue | 4 +- .../src/components/MkEmojiPickerWindow.vue | 49 --- .../frontend/src/components/MkFormDialog.vue | 56 ++-- packages/frontend/src/os.ts | 304 ++++++++++-------- .../frontend/src/pages/emoji-edit-dialog.vue | 2 +- packages/frontend/src/pages/notifications.vue | 4 +- .../frontend/src/pages/settings/drive.vue | 2 +- .../src/pages/settings/emoji-picker.vue | 2 +- .../pages/settings/preferences-backups.vue | 2 + packages/frontend/src/scripts/form.ts | 17 +- packages/frontend/src/ui/deck.vue | 20 +- .../frontend/src/widgets/WidgetSlideshow.vue | 4 +- 16 files changed, 257 insertions(+), 250 deletions(-) delete mode 100644 packages/frontend/src/components/MkEmojiPickerWindow.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index f52226a635..bd35691871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ - Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正 - Fix: チャートのラベルが消えている問題を修正 - Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正 +- Fix: 設定のバックアップ作成時に名前を入力しなかった場合、ローカライゼーションがおかしくなる問題を修正 +- Fix: ページ`/admin/emojis`の絵文字編集ダイアログで「リアクションとして使えるロール」を追加する際に何も選択せずOKを押下すると画面が固まる問題を修正 - Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正 ### Server diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index e606fe368c..7f20e0b1a2 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -290,7 +290,7 @@ export async function openAccountMenu(opts: { text: i18n.ts.profile, to: `/@${ $i.username }`, avatar: $i, - }, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { + }, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { type: 'parent' as const, icon: 'ti ti-plus', text: i18n.ts.addAccount, diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 4b7584faaa..4577d37c08 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -38,11 +38,6 @@ SPDX-License-Identifier: AGPL-3.0-only -
{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }} @@ -64,7 +59,7 @@ import MkSelect from '@/components/MkSelect.vue'; import { i18n } from '@/i18n.js'; type Input = { - type: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local'; + type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local'; placeholder?: string | null; autocomplete?: string; default: string | number | null; @@ -74,22 +69,17 @@ type Input = { type Select = { items: { - value: string; + value: any; text: string; }[]; - groupedItems: { - label: string; - items: { - value: string; - text: string; - }[]; - }[]; default: string | null; }; +type Result = string | number | true | null; + const props = withDefaults(defineProps<{ type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting'; - title: string; + title?: string; text?: string; input?: Input; select?: Select; @@ -113,7 +103,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'done', v: { canceled: boolean; result: any }): void; + (ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void; (ev: 'closed'): void; }>(); @@ -139,8 +129,11 @@ const okButtonDisabledReason = computed(); const dialog = shallowRef>(); -const selected = ref([]); +const selected = ref([]); function ok() { emit('done', selected.value); @@ -57,7 +57,7 @@ function cancel() { dialog.value?.close(); } -function onChangeSelection(files: Misskey.entities.DriveFile[]) { - selected.value = files; +function onChangeSelection(v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]) { + selected.value = v; } diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 59f4b51522..adcea839ee 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'done', v: any): void; + (ev: 'done', v: string): void; (ev: 'close'): void; (ev: 'closed'): void; }>(); @@ -64,7 +64,7 @@ const emit = defineEmits<{ const modal = shallowRef>(); const picker = shallowRef>(); -function chosen(emoji: any) { +function chosen(emoji: string) { emit('done', emoji); if (props.choseAndClose) { modal.value?.close(); diff --git a/packages/frontend/src/components/MkEmojiPickerWindow.vue b/packages/frontend/src/components/MkEmojiPickerWindow.vue deleted file mode 100644 index 6952943345..0000000000 --- a/packages/frontend/src/components/MkEmojiPickerWindow.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 0d8734799c..deedc5badb 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -21,37 +21,37 @@ SPDX-License-Identifier: AGPL-3.0-only
-