diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-02-26 20:21:54 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-02-26 20:21:54 +0900 |
| commit | 02c8fd9de51b6a8471ab9a89f23bcfeaecd7626c (patch) | |
| tree | 018a46cad9a19cc8cdfcff91442b343a637b56f8 /packages/backend/src/server/api/endpoints/notes | |
| parent | Merge pull request #10058 from misskey-dev/develop (diff) | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | misskey-02c8fd9de51b6a8471ab9a89f23bcfeaecd7626c.tar.gz misskey-02c8fd9de51b6a8471ab9a89f23bcfeaecd7626c.tar.bz2 misskey-02c8fd9de51b6a8471ab9a89f23bcfeaecd7626c.zip | |
Merge pull request #10108 from misskey-dev/develop
* Add dialog to remove follower (#9718)
* update PULL_REQUEST_TEMPLATE
* 起動時にRedisの疎通確認を行う (#9832)
* 起動時にRedisの疎通確認を行う
* check:connectをstart内に移動
---------
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
* Pass `--detectOpenHandles` to Jest (#9895)
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
* enhance(client): MkUrlPreviewの閉じるボタンを見やすく (#9913)
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
* test(backend): restore ap-request tests (#9997)
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
* fix/refaftor(client): MkTime.vueの変更 (#10061)
* fix(client): MkTime.timeにstringでもDateでない値が入った場合、?を表示
* fix(client): MkTimeを改良
* numberを許容
* falsyな値もとる
* 不明
* ありません
* fix
* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする (#9911)
* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする
* NO_SUCH_FILE
* Update codecov.yml
* Update apple-touch-icon.png
* デプロイされているプレビュー環境がない場合はプレビュー環境を削除しないようにする (#10062)
* デプロイされているプレビュー環境がない場合はDestroy preview environmentを実行しないようにする
* CIがない場合の処理追加
* enhance(client): improve clip menu ux
* 未知のユーザーが deleteActor されたら処理をスキップする (#10067)
* fix(client): Android ChromeでPWAとしてインストールできない問題を修正 (#10069)
* fix(client): Android ChromeでPWAとしてインストールできない問題を修正
* 順番関係ある?
* Windows環境でswcを使うと正常にビルドができない問題の修正 (#10074)
* Update @swc/core to v1.3.36
* Update CHANGELOG.md
* Update CHANGELOG.md
* バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない (#10053)
* :art:
* feat: 2つの検索画面の統合 (#9949) (#10038)
* feat: 検索画面の UI を統一
* fix: エラーの修正
* add: changelog
---------
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
* enhance(client): ノートメニューからユーザーメニューを開けるように
Resolve #10019
* enhance(client): renoteした際の表示を改善
Resolve #10078
* Update CHANGELOG.md
* enhance(client): tweak contextmenu position calculation
* :art:
* :art:
* feat: in-channel featured note
Resolve #9938
* refactor(frontend): fix eslint error (#10084)
* Simplify search.vue (remove dead code) (#10088)
* Simplify search.vue
This is already handled by the code above it, no need to handle it twice
* Remove unused imports
* Update about-misskey.vue
* test(server): add validation test of api:notes/create (#10090)
* fix(server): notes/createのバリデーションが効いていない
Fix #10079
Co-Authored-By: mei23 <m@m544.net>
* anyOf内にバリデーションを書いても最初の一つしかチェックされない
* :v:
* wip
* wip
* :v:
* RequiredProp
* Revert "RequiredProp"
This reverts commit 74693900119a590263106fa3adefd008d69ce80c.
* add api:notes/create
* fix lint
* text
* :v:
* improve readability
---------
Co-authored-by: mei23 <m@m544.net>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
* New Crowdin updates (#10059)
* New translations ja-JP.yml (Japanese, Kansai)
* New translations ja-JP.yml (Romanian)
* New translations ja-JP.yml (French)
* New translations ja-JP.yml (Spanish)
* New translations ja-JP.yml (Arabic)
* New translations ja-JP.yml (Czech)
* New translations ja-JP.yml (German)
* New translations ja-JP.yml (Italian)
* New translations ja-JP.yml (Korean)
* New translations ja-JP.yml (Polish)
* New translations ja-JP.yml (Russian)
* New translations ja-JP.yml (Slovak)
* New translations ja-JP.yml (Ukrainian)
* New translations ja-JP.yml (Chinese Simplified)
* New translations ja-JP.yml (Chinese Traditional)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (Vietnamese)
* New translations ja-JP.yml (Indonesian)
* New translations ja-JP.yml (Bengali)
* New translations ja-JP.yml (Thai)
* New translations ja-JP.yml (Japanese, Kansai)
* New translations ja-JP.yml (Chinese Traditional)
* New translations ja-JP.yml (Chinese Traditional)
* New translations ja-JP.yml (German)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (German)
* New translations ja-JP.yml (Ukrainian)
* New translations ja-JP.yml (Chinese Simplified)
* New translations ja-JP.yml (English)
* New translations ja-JP.yml (Ukrainian)
* New translations ja-JP.yml (Ukrainian)
* New translations ja-JP.yml (Ukrainian)
* New translations ja-JP.yml (Chinese Simplified)
* New translations ja-JP.yml (Japanese, Kansai)
* New translations ja-JP.yml (Thai)
* New translations ja-JP.yml (Thai)
* New translations ja-JP.yml (Thai)
* New translations ja-JP.yml (Spanish)
* New translations ja-JP.yml (Spanish)
* enhance(client): improve user menu ux
* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように (#10098)
* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように
* add: changelog
---------
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
* enhance(client): メニューの「もっと」からインスタンス情報を見れるように
* [Fix] fixed an typo in error message (#10102)
* Update codecov.yml
* Update CHANGELOG.md
* fix(server): エラーのスタックトレースは返さないように
Fix #10064
* [chore]Editorconfig: ymlに加えてyamlファイルに対しても同じ規約を適用する (#10081)
* Added yaml file in addition to yml file, in editorconfig
* Applied editorconfig for pnpm-workspace.yaml
---------
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
* update deps
* ホームタイムラインの読み込みでクエリタイムアウトになるのを修正する (#10106)
* refactor
* New translations ja-JP.yml (French) (#10103)
* Update CHANGELOG.md
* 13.8.0
---------
Co-authored-by: atsuchan <83960488+atsu1125@users.noreply.github.com>
Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
Co-authored-by: xianon <xianon@hotmail.co.jp>
Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com>
Co-authored-by: YS <47836716+yszkst@users.noreply.github.com>
Co-authored-by: Khsmty <me@khsmty.com>
Co-authored-by: Soni L <EnderMoneyMod@gmail.com>
Co-authored-by: mei23 <m@m544.net>
Co-authored-by: daima3629 <52790780+daima3629@users.noreply.github.com>
Co-authored-by: Windymelt <1113940+windymelt@users.noreply.github.com>
Diffstat (limited to 'packages/backend/src/server/api/endpoints/notes')
5 files changed, 346 insertions, 98 deletions
diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts new file mode 100644 index 0000000000..6bff7fc0c9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -0,0 +1,263 @@ +process.env.NODE_ENV = 'test'; + +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import { describe, test, expect } from '@jest/globals'; +import { getValidator } from '../../../../../test/prelude/get-api-validator.js'; +import { paramDef } from './create.js'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + +const VALID = true; +const INVALID = false; + +describe('api:notes/create', () => { + describe('validation', () => { + const v = getValidator(paramDef); + const tooLong = readFile(_dirname + '/../../../../../test/resources/misskey.svg', 'utf-8'); + + test('reject empty', () => { + const valid = v({ }); + expect(valid).toBe(INVALID); + }); + + describe('text', () => { + test('simple post', () => { + expect(v({ text: 'Hello, world!' })) + .toBe(VALID); + }); + + test('null post', () => { + expect(v({ text: null })) + .toBe(INVALID); + }); + + test('0 characters post', () => { + expect(v({ text: '' })) + .toBe(INVALID); + }); + + test('over 3000 characters post', async () => { + expect(v({ text: await tooLong })) + .toBe(INVALID); + }); + }); + + describe('cw', () => { + test('simple cw', () => { + expect(v({ text: 'Hello, world!', cw: 'Hello, world!' })) + .toBe(VALID); + }); + + test('null cw', () => { + expect(v({ text: 'Body', cw: null })) + .toBe(VALID); + }); + + test('0 characters cw', () => { + expect(v({ text: 'Body', cw: '' })) + .toBe(VALID); + }); + + test('reject only cw', () => { + expect(v({ cw: 'Hello, world!' })) + .toBe(INVALID); + }); + + test('over 100 characters cw', async () => { + expect(v({ text: 'Body', cw: await tooLong })) + .toBe(INVALID); + }); + }); + + describe('visibility', () => { + test('public', () => { + expect(v({ text: 'Hello, world!', visibility: 'public' })) + .toBe(VALID); + }); + + test('home', () => { + expect(v({ text: 'Hello, world!', visibility: 'home' })) + .toBe(VALID); + }); + + test('followers', () => { + expect(v({ text: 'Hello, world!', visibility: 'followers' })) + .toBe(VALID); + }); + + test('reject only visibility', () => { + expect(v({ visibility: 'public' })) + .toBe(INVALID); + }); + + test('reject invalid visibility', () => { + expect(v({ text: 'Hello, world!', visibility: 'invalid' })) + .toBe(INVALID); + }); + + test('reject null visibility', () => { + expect(v({ text: 'Hello, world!', visibility: null })) + .toBe(INVALID); + }); + + describe('visibility:specified', () => { + test('specified without visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified' })) + .toBe(VALID); + }); + + test('specified with empty visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: [] })) + .toBe(VALID); + }); + + test('reject specified with non unique visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: ['1', '1', '2'] })) + .toBe(INVALID); + }); + + test('reject specified with null visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: null })) + .toBe(INVALID); + }); + }); + }); + + describe('fileIds', () => { + test('only fileIds', () => { + expect(v({ fileIds: ['1', '2', '3'] })) + .toBe(VALID); + }); + + test('text and fileIds', () => { + expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'] })) + .toBe(VALID); + }); + + test('reject null fileIds', () => { + expect(v({ fileIds: null })) + .toBe(INVALID); + }); + + test('reject text and null fileIds (複合的なanyOfのバリデーションが正しく動作する)', () => { + expect(v({ text: 'Hello, world!', fileIds: null })) + .toBe(INVALID); + }); + + test('reject 0 files', () => { + expect(v({ fileIds: [] })) + .toBe(INVALID); + }); + + test('reject non unique', () => { + expect(v({ fileIds: ['1', '1', '2'] })) + .toBe(INVALID); + }); + + test('reject invalid id', () => { + expect(v({ fileIds: ['あ'] })) + .toBe(INVALID); + }); + + test('reject over 17 files', () => { + const valid = v({ text: 'Hello, world!', fileIds: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18'] }); + expect(valid).toBe(INVALID); + }); + }); + + describe('poll', () => { + test('note with poll', () => { + expect(v({ text: 'Hello, world!', poll: { choices: ['a', 'b', 'c'] } })) + .toBe(VALID); + }); + + test('null poll', () => { + expect(v({ text: 'Hello, world!', poll: null })) + .toBe(VALID); + }); + + test('allow only poll', () => { + expect(v({ poll: { choices: ['a', 'b', 'c'] } })) + .toBe(VALID); + }); + + test('poll with expiresAt', async () => { + expect(v({ poll: { choices: ['a', 'b', 'c'], expiresAt: 1 } })) + .toBe(VALID); + }); + + test('poll with expiredAfter', async () => { + expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 1 } })) + .toBe(VALID); + }); + + test('reject poll without choices', () => { + expect(v({ poll: { } })) + .toBe(INVALID); + }); + + test('reject poll with empty choices', () => { + expect(v({ poll: { choices: [] } })) + .toBe(INVALID); + }); + + test('reject poll with null choices', () => { + expect(v({ poll: { choices: null } })) + .toBe(INVALID); + }); + + test('reject poll with 1 choice', () => { + expect(v({ poll: { choices: ['a'] } })) + .toBe(INVALID); + }); + + test('reject poll with too long choice', async () => { + expect(v({ poll: { choices: [await tooLong, '2'] } })) + .toBe(INVALID); + }); + + test('reject poll with too many choices', () => { + expect(v({ poll: { choices: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] } })) + .toBe(INVALID); + }); + + test('reject poll with non unique choices', () => { + expect(v({ poll: { choices: ['a', 'a', 'b', 'c'] } })) + .toBe(INVALID); + }); + + test('reject poll with expiredAfter 0', async () => { + expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 0 } })) + .toBe(INVALID); + }); + }); + + describe('renote', () => { + test('just a renote', () => { + expect(v({ renoteId: '1' })) + .toBe(VALID); + }); + test('just a quote', () => { + expect(v({ text: 'Hello, world!', renoteId: '1' })) + .toBe(VALID); + }); + test('reject invalid renoteId', () => { + expect(v({ renoteId: 'あ' })) + .toBe(INVALID); + }); + }); + + test('text, fileIds and poll', () => { + expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'], poll: { choices: ['a', 'b', 'c'] } })) + .toBe(VALID); + }); + + test('text, invalid fileIds and invalid poll', () => { + expect(v({ text: 'Hello, world!', fileIds: ['あ'], poll: { choices: ['a'] } })) + .toBe(INVALID); + }); + }); +}); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 593444968e..786ad103b0 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -79,6 +79,12 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', }, + + noSuchFile: { + message: 'Some files are not found.', + code: 'NO_SUCH_FILE', + id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', + }, }, } as const; @@ -95,74 +101,56 @@ export const paramDef = { noExtractHashtags: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false }, replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true }, - }, - anyOf: [ - { - // (re)note with text, files and poll are optional - properties: { - text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, - }, - required: ['text'], + + // anyOf内にバリデーションを書いても最初の一つしかチェックされない + // See https://github.com/misskey-dev/misskey/pull/10082 + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: false }, - { - // (re)note with files, text and poll are optional - properties: { - fileIds: { - type: 'array', - uniqueItems: true, - minItems: 1, - maxItems: 16, - items: { type: 'string', format: 'misskey:id' }, - }, - }, - required: ['fileIds'], + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, }, - { - // (re)note with files, text and poll are optional + mediaIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + poll: { + type: 'object', + nullable: true, properties: { - mediaIds: { - deprecated: true, - description: 'Use `fileIds` instead. If both are specified, this property is discarded.', + choices: { type: 'array', uniqueItems: true, - minItems: 1, - maxItems: 16, - items: { type: 'string', format: 'misskey:id' }, - }, - }, - required: ['mediaIds'], - }, - { - // (re)note with poll, text and files are optional - properties: { - poll: { - type: 'object', - nullable: true, - properties: { - choices: { - type: 'array', - uniqueItems: true, - minItems: 2, - maxItems: 10, - items: { type: 'string', minLength: 1, maxLength: 50 }, - }, - multiple: { type: 'boolean' }, - expiresAt: { type: 'integer', nullable: true }, - expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, - }, - required: ['choices'], + minItems: 2, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, }, - required: ['poll'], - }, - { - // pure renote - properties: { - renoteId: { type: 'string', format: 'misskey:id', nullable: true }, - }, - required: ['renoteId'], + required: ['choices'], }, + }, + // (re)note with text, files and poll are optional + anyOf: [ + { required: ['text'] }, + { required: ['renoteId'] }, + { required: ['fileIds'] }, + { required: ['mediaIds'] }, + { required: ['poll'] }, ], } as const; @@ -207,6 +195,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') .setParameters({ fileIds }) .getMany(); + + if (files.length !== fileIds.length) { + throw new ApiError(meta.errors.noSuchFile); + } } let renote: Note | null = null; diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 26f69373d1..cf939f6631 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -28,6 +28,7 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, + channelId: { type: 'string', nullable: true, format: 'misskey:id' }, }, required: [], } as const; @@ -63,6 +64,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); + if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index bcd793ac43..da1a4bcc46 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -36,32 +36,25 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - }, - anyOf: [ - { - properties: { - tag: { type: 'string', minLength: 1 }, - }, - required: ['tag'], - }, - { - properties: { - query: { - type: 'array', - description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', - items: { - type: 'array', - items: { - type: 'string', - minLength: 1, - }, - minItems: 1, - }, - minItems: 1, + + tag: { type: 'string', minLength: 1 }, + query: { + type: 'array', + description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', + items: { + type: 'array', + items: { + type: 'string', + minLength: 1, }, + minItems: 1, }, - required: ['query'], + minItems: 1, }, + }, + anyOf: [ + { required: ['tag'] }, + { required: ['query'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index d1c35e36e2..e6de087c4a 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -58,25 +58,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const hasFollowing = (await this.followingsRepository.count({ - where: { - followerId: me.id, - }, - take: 1, - })) !== 0; - - //#region Construct query - const followingQuery = this.followingsRepository.createQueryBuilder('following') + const followees = await this.followingsRepository.createQueryBuilder('following') .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); + .where('following.followerId = :followerId', { followerId: me.id }) + .getMany(); + //#region Construct query const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30日前まで - .andWhere(new Brackets(qb => { qb - .where('note.userId = :meId', { meId: me.id }); - if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`); - })) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -87,8 +77,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .setParameters(followingQuery.getParameters()); + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + + if (followees.length > 0) { + const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; + + query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); + } else { + query.andWhere('note.userId = :meId', { meId: me.id }); + } this.queryService.generateChannelQuery(query, me); this.queryService.generateRepliesQuery(query, me); |