diff --git a/CHANGELOG.md b/CHANGELOG.md index 800c646c67..15028e7008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,12 @@ - --> -## 202x.x.x (unreleased) + +## 2024.3.0 ### General - Enhance: 投稿者のロールに応じて、一つのノートに含むことのできるメンションとダイレクト投稿の宛先の人数に上限を設定できるように - * デフォルトのメンション上限は20アカウントに設定されます。(管理者はベースロールの設定で変更可能です。) + * デフォルトのメンション上限は20アカウントに設定されます。(管理者はベースロールの設定で変更可能です。) * 連合の問い合わせに応答しないサーバーのリモートユーザーへのメンションは、上限の人数に含めない実装になっています。 - Enhance: 通知がミュート、凍結を考慮するようになりました - Enhance: サーバーごとにモデレーションノートを残せるように @@ -33,6 +34,7 @@ - Fix: 設定のバックアップ作成時に名前を入力しなかった場合、ローカライゼーションがおかしくなる問題を修正 - Fix: ページ`/admin/emojis`の絵文字編集ダイアログで「リアクションとして使えるロール」を追加する際に何も選択せずOKを押下すると画面が固まる問題を修正 - Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正 +- Fix: ユーザの情報のポップアップが消えなくなることがある問題を修正 ### Server - Enhance: エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました @@ -119,7 +121,6 @@ - Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正 - Fix: MkCodeEditorで行がずれていってしまう問題の修正 - Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196 -- Fix: ユーザの情報のポップアップが消えなくなることがある問題を修正 ### Server - Enhance: 連合先のレートリミットを超過した際にリトライするようになりました diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index bc872823f1..5cdecc10ac 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1655,6 +1655,7 @@ _role: gtlAvailable: "瀏覽全域時間軸" ltlAvailable: "瀏覽本地時間軸" canPublicNote: "允許公開貼文" + mentionMax: "貼文內的最大提及數" canInvite: "發行伺服器邀請碼" inviteLimit: "可建立邀請碼的數量" inviteLimitCycle: "邀請碼的發放間隔" @@ -2299,6 +2300,7 @@ _notification: reactedBySomeUsers: "{n}人做出了反應" renotedBySomeUsers: "{n}人做了轉發" followedBySomeUsers: "被{n}人追隨了" + flushNotification: "重置通知歷史紀錄" _types: all: "全部 " note: "使用者的最新貼文" diff --git a/package.json b/package.json index 68814f74b8..dee4645ee3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.2.0", + "version": "2024.3.0", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 727787f868..81ae2908d3 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -263,7 +263,13 @@ export class NoteCreateService implements OnApplicationShutdown { } } - if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) { + const hasProhibitedWords = await this.checkProhibitedWordsContain({ + cw: data.cw, + text: data.text, + pollChoices: data.poll?.choices, + }, meta.prohibitedWords); + + if (hasProhibitedWords) { throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } @@ -995,6 +1001,23 @@ export class NoteCreateService implements OnApplicationShutdown { } } + public async checkProhibitedWordsContain(content: Parameters[0], prohibitedWords?: string[]) { + if (prohibitedWords == null) { + prohibitedWords = (await this.metaService.fetch()).prohibitedWords; + } + + if ( + this.utilityService.isKeyWordIncluded( + this.utilityService.concatNoteContentsForKeyWordCheck(content), + prohibitedWords, + ) + ) { + return true; + } + + return false; + } + @bindThis public dispose(): void { this.#shutdownController.abort(); diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 638a0c019e..652e8f7449 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -42,6 +42,20 @@ export class UtilityService { return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); } + @bindThis + public concatNoteContentsForKeyWordCheck(content: { + cw?: string | null; + text?: string | null; + pollChoices?: string[] | null; + others?: string[] | null; + }): string { + /** + * ノートの内容を結合してキーワードチェック用の文字列を生成する + * cwとtextは内容が繋がっているかもしれないので間に何も入れずにチェックする + */ + return `${content.cw ?? ''}${content.text ?? ''}\n${(content.pollChoices ?? []).join('\n')}\n${(content.others ?? []).join('\n')}`; + } + @bindThis public isKeyWordIncluded(text: string, keyWords: string[]): boolean { if (keyWords.length === 0) return false; diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index e201b88173..b2fd435f93 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -24,6 +24,8 @@ import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; @@ -37,7 +39,6 @@ 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 { @@ -152,11 +153,47 @@ export class ApNoteService { throw new Error('invalid note.attributedTo: ' + note.attributedTo); } - const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser; + const uri = getOneApId(note.attributedTo); - // 投稿者が凍結されていたらスキップ + // ローカルで投稿者を検索し、もし凍結されていたらスキップ + const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser; + if (cachedActor && cachedActor.isSuspended) { + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + } + + const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); + const apHashtags = extractApHashtags(note.tag); + + const cw = note.summary === '' ? null : note.summary; + + // テキストのパース + let text: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { + text = note.source.content; + } else if (typeof note._misskey_content !== 'undefined') { + text = note._misskey_content; + } else if (typeof note.content === 'string') { + text = this.apMfmService.htmlToMfm(note.content, note.tag); + } + + const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); + + //#region Contents Check + // 添付ファイルとユーザーをこのサーバーで登録する前に内容をチェックする + /** + * 禁止ワードチェック + */ + const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); + if (hasProhibitedWords) { + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); + } + //#endregion + + const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; + + // 解決した投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new Error('actor has been suspended'); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); @@ -171,9 +208,6 @@ export class ApNoteService { } } - const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); - const apHashtags = extractApHashtags(note.tag); - // 添付ファイル // TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしも配列ではない @@ -233,18 +267,6 @@ export class ApNoteService { } } - const cw = note.summary === '' ? null : note.summary; - - // テキストのパース - let text: string | null = null; - if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { - text = note.source.content; - } else if (typeof note._misskey_content !== 'undefined') { - text = note._misskey_content; - } else if (typeof note.content === 'string') { - text = this.apMfmService.htmlToMfm(note.content, note.tag); - } - // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); @@ -274,8 +296,6 @@ export class ApNoteService { const apEmojis = emojis.map(emoji => emoji.name); - const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); - try { return await this.noteCreateService.create(actor, { createdAt: note.published ? new Date(note.published) : null, diff --git a/packages/backend/src/misc/FileWriterStream.ts b/packages/backend/src/misc/FileWriterStream.ts index 828851df0e..367a8eb560 100644 --- a/packages/backend/src/misc/FileWriterStream.ts +++ b/packages/backend/src/misc/FileWriterStream.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs/promises'; import type { PathLike } from 'node:fs'; diff --git a/packages/backend/src/misc/JsonArrayStream.ts b/packages/backend/src/misc/JsonArrayStream.ts index ad35bb3a79..754938989d 100644 --- a/packages/backend/src/misc/JsonArrayStream.ts +++ b/packages/backend/src/misc/JsonArrayStream.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { TransformStream } from 'node:stream/web'; /** diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 4d7f76c0d9..3f10a128ed 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -184,7 +184,10 @@ export class InboxProcessorService { 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'; + if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { + return 'blocked notes with prohibited words'; + } + if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') return 'actor has been suspended'; } throw e; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index d894ef730e..929070d0d2 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -18,7 +18,6 @@ * achievementEarned - 実績を獲得 * app - アプリ通知 * test - テスト通知(サーバー側) - * */ export const notificationTypes = [ 'note', diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index 1e4225184a..1d28e07b7d 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -117,6 +117,7 @@ describe('Mute', () => { assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); }); + test('通知にミュートしているユーザーからのリプライが含まれない', async () => { const aliceNote = await post(alice, { text: 'hi' }); await post(bob, { text: '@alice hi', replyId: aliceNote.id }); diff --git a/packages/frontend/src/scripts/search-emoji.ts b/packages/frontend/src/scripts/search-emoji.ts index 07f55e5531..371f69b9a7 100644 --- a/packages/frontend/src/scripts/search-emoji.ts +++ b/packages/frontend/src/scripts/search-emoji.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export type EmojiDef = { emoji: string; name: string; diff --git a/packages/frontend/test/autocomplete.test.ts b/packages/frontend/test/autocomplete.test.ts index f6a7ce9450..394ac3a821 100644 --- a/packages/frontend/test/autocomplete.test.ts +++ b/packages/frontend/test/autocomplete.test.ts @@ -7,28 +7,28 @@ 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('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('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('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('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:'); - }); + test('名前の部分一致はタグの部分一致より優先される', async () => { + const result = searchEmoji('oooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]); + assert.equal(result[0].emoji, ':foooo:'); + }); }); diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 1069e85b23..a7c629119c 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.2.0", + "version": "2024.3.0", "description": "Misskey SDK for JavaScript", "types": "./built/dts/index.d.ts", "exports": {