Compare commits

...

74 commits

Author SHA1 Message Date
dakkar
e1a638fd78 merge: Draft: some future changes coming from upstream (!465)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/465
2024-03-14 16:36:20 +00:00
dakkar
760df9ebf0 copy changes to SkNote* 2024-03-08 16:10:52 +00:00
dakkar
15c78d1a01 Merge remote-tracking branch 'misskey/develop' into future 2024-03-08 16:09:56 +00:00
かっこかり
27f823e882
enhance(frontend): リアクションの総数を表示するか設定で選べるように (#13539)
* enhance(frontend): リプライ・リノート・リアクションの総数を表示するか設定で選べるように (MisskeyIO#512)

(cherry picked from commit 3c8475e5ac217f055eab0f6d0aedcbbcb2a2f27c)

* fix: いいねのみの場合は強制的にカウント表示

* make `showReactionsCount` default false

* リアクションだけ

* けしわすれ

* けしわすれ2

---------

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-03-08 18:13:09 +09:00
dakkar
8ab3f3aad6 copy changes to SkNote* 2024-03-07 16:20:43 +00:00
dakkar
dfff4d2073 Merge remote-tracking branch 'misskey/develop' into future 2024-03-07 16:09:36 +00:00
かっこかり
f4a5740412
fix(frontend): 周年の実績が閏年を考慮するように (#13525)
* fix(frontend): 周年の実績が閏年を考慮するように

* まちがえた

* Update Changelog

* 変数の定義回数を減らす
2024-03-07 17:21:57 +09:00
かっこかり
c680e35aa0
enhance(frontend): 広告が同一ドメインの場合はRouterで遷移するように (#13510)
* enhance(frontend): 広告が同一ドメインの場合はRouterで遷移するように

* Update Changelog

* Update CHANGELOG.md

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-03-07 16:36:06 +09:00
zyoshoka
412e9f284d
test(backend): enable typecheck by workflow (#13526) 2024-03-07 09:51:57 +09:00
かっこかり
7ead98cbe5
enhance(frontend): リアクションの総数を表示するように (#13532)
* enhance(frontend): リアクションの総数を表示するように

* Update Changelog

* リアクション選択済の色をaccentに
2024-03-06 21:08:42 +09:00
tamaina
62922352b3 Revert "perf: boot.jsの調整"
This reverts commit 00c1e4eb55.
2024-03-06 09:49:01 +00:00
tamaina
00c1e4eb55 perf: boot.jsの調整 2024-03-06 09:40:47 +00:00
tamaina
4457b02db2 fix(frontend)?: importAppScriptはimportをawaitするように 2024-03-06 08:08:32 +00:00
かっこかり
08d618bb8b
enhance(frontend): 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるようにする (#13520)
* enhance(frontend): 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるようにする

* 他のファイルタイプにも対応

* Update Changelog

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-03-05 18:06:57 +09:00
かっこかり
45672a70f9
fix(frontend): router遷移時にmatchAllに入った場合一度location.hrefを経由するように (#13509)
* fix(frontend): router遷移時にmatchAllに入った場合一度`location.href`を経由するように

* Update Changelog

* Update CHANGELOG.md

* remove unnecessary args
2024-03-05 17:27:33 +09:00
tamaina
83a5bc0ecd
doc: Nestで循環依存がある場合のCONTRIBUTING.mdに書く (#13522)
* doc: Nestモジュールテストの例をCONTRIBUTING.mdに書く

* rm normal test

* forwardRef
2024-03-05 14:26:16 +09:00
tamaina
13f5fafdbc remove template txt 2024-03-04 10:39:43 +00:00
かっこかり
96ab1af03b
Update CHANGELOG.md 2024-03-04 16:09:24 +09:00
tamaina
9542cb8d62
fix(backend): リモートサーバーの情報が更新できなくなっていた問題を修正 (#13507)
* fix(backend): fetchInstanceMetadataのLockが永遠に解除されない問題を修正

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>

* fix test

* fix

* comment

* comment

* improve test

---------

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-03-04 13:48:57 +09:00
tamaina
983480131b
chore: Automated release (#13075)
* chore: Automated release

* follow
2024-03-04 12:54:13 +09:00
zyoshoka
38837bd388
test(backend): refactor tests (#13499)
* test(backend): refactor tests

* fix: failed test
2024-03-03 20:15:35 +09:00
syuilo
efda2e9baa
Update README.md 2024-03-02 18:34:49 +09:00
syuilo
3afdafed61 2024.3.1 2024-03-02 17:06:01 +09:00
tamaina
2744cbd310 fix(frontend): MkCustomEmojiでフォールバックをテキストか画像か選べるように
fix of #13487
2024-03-02 07:05:17 +00:00
おさむのひと
b83cbc6d4d
Update CHANGELOG.md 2024-03-02 13:39:49 +09:00
おさむのひと
21e3a91393
Update CHANGELOG.md 2024-03-02 13:35:33 +09:00
syuilo
ecc5decaa5
New Crowdin updates (#13489)
* New translations ja-jp.yml (French)

* New translations ja-jp.yml (French)
2024-03-02 13:28:22 +09:00
おさむのひと
32690f576f
fix(frontend): ピン留め or 履歴に表示されるカスタム絵文字がサーバから削除されるとリアクションが出来なくなる (#13486)
* fix(frontend): ピン留めに登録されているカスタム絵文字がサーバから削除されるとリアクションが出来なくなる

* fix CHANGELOG.md

* fix Unicode Emojis

* fix Unicode Emojis

* fix
2024-03-02 13:28:10 +09:00
tamaina
114d3319e8
chore(client): 絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 (#13487) 2024-03-02 13:26:21 +09:00
Acid Chicken (硫酸鶏)
f704891932
fix: emoji colorization 2024-03-02 05:53:43 +09:00
syuilo
fe5efd926e
New translations ja-jp.yml (Chinese Traditional) (#13480) 2024-03-01 21:00:43 +09:00
syuilo
ba9d47fb69 2024.3.0 2024-03-01 20:22:06 +09:00
tamaina
eb60460d28
enhance: 禁止ワードチェック強化 (#27)
* enhance: 禁止ワードチェック強化
* リモートの禁止ワードチェックを添付ファイルとユーザーを登録する前に行うなど
  Resolve https://github.com/misskey-dev/misskey/issues/13374
* 禁止ワートの対象の見直し

* performActivityで特定のエラーが出た際にDelayedに追加しないように

* use IdentifiableError

* NoteCreateService.checkProhibitedWords

* https://github.com/misskey-dev/misskey-private/pull/27/files#r1507416135

* remove comment
2024-03-01 20:16:32 +09:00
syuilo
d1bf432e14 add missing license headers 2024-03-01 17:28:46 +09:00
syuilo
4c6fc15858 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-03-01 17:27:11 +09:00
syuilo
6158ef138e format 2024-03-01 17:27:03 +09:00
syuilo
5904d98208
Update packages/backend/test/e2e/mute.ts
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2024-03-01 17:26:27 +09:00
syuilo
ca6399437c format 2024-03-01 17:26:13 +09:00
syuilo
5befd66e21 Update CHANGELOG.md 2024-03-01 17:25:54 +09:00
syuilo
16440d6be2
Update CHANGELOG.md
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2024-03-01 17:24:59 +09:00
syuilo
2f31606eff update deps 2024-03-01 14:16:44 +09:00
syuilo
14a3af679d update deps 2024-03-01 14:06:34 +09:00
syuilo
033d71ee28 update deps 2024-03-01 13:52:39 +09:00
syuilo
b55b77c8ae update pnpm 2024-03-01 13:52:23 +09:00
syuilo
59f80c08ea
New Crowdin updates (#13478)
* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)
2024-03-01 12:07:25 +09:00
syuilo
a74406677c fix packedRoleCondFormulaValueAssignedRoleSchema 2024-03-01 12:03:33 +09:00
tamaina
593358ed3f Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-02-29 11:49:49 +00:00
tamaina
bc30dc6bff refactor: remove export of unicodeEmojisMap 2024-02-29 11:49:40 +00:00
syuilo
01f55a9d59
Update CHANGELOG.md 2024-02-29 20:48:48 +09:00
Yuriha
26d4c5fd94
メンションの最大数をロールごとに設定可能にする (#13343)
* Add new role policy: maximum mentions per note

* fix

* Reviewを反映

* fix

* Add ChangeLog

* Update type definitions

* Add E2E test

* CHANGELOG に説明を追加

---------

Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
2024-02-29 20:48:02 +09:00
tamaina
b9bcceddfc Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-02-29 11:47:30 +00:00
tamaina
7565f7bec6 fix(client): use colorizeEmoji when unicodeEmojisMap.get 2024-02-29 11:47:24 +00:00
syuilo
6365805687
New Crowdin updates (#13359)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* 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 (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Korean (Gyeongsang))

* New translations ja-jp.yml (Chinese Simplified)

* 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 (Italian)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* 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 (Thai)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)
2024-02-29 20:44:32 +09:00
syuilo
1205d30657
Update CHANGELOG.md 2024-02-29 20:42:58 +09:00
zyoshoka
16f16e6b08
fix(backend): ダイレクトなノートに対してはダイレクトでしか返信できないように (#13477)
* fix(backend): ダイレクトなノートに対してはダイレクトでしか返信できないように

* Update CHANGELOG.md

* test(backend): `notes/create`とWebSocket関連のテストを追加
2024-02-29 20:42:02 +09:00
かっこかり
39d6af135f
enhance: 通知の履歴をリセットできるように (#13335)
* enhance: 通知の履歴をリセットできるように

* Update Changelog

* 通知欄も連動して更新するように

* revert some changes

* Update CHANGELOG.md

* Remove unused part

* fix
2024-02-29 20:03:30 +09:00
syuilo
ec18991328 Update scroll.test.ts 2024-02-29 19:44:00 +09:00
syuilo
9d0fc96d1a fix test 2024-02-29 18:04:03 +09:00
syuilo
98934b6738 fix type 2024-02-29 17:54:32 +09:00
syuilo
920c3be750 update deps 2024-02-29 11:10:03 +09:00
syuilo
797bb493ab
Update CHANGELOG.md 2024-02-29 10:20:37 +09:00
taichan
5f43c2faa2
enhance(backend): 通知がミュート・凍結を考慮するようにする (#13412)
* Never return broken notifications #409

Since notifications are stored in Redis, we can't expect relational
integrity: deleting a user will *not* delete notifications that
mention it.

But if we return notifications with missing bits (a `follow` without a
`user`, for example), the frontend will get very confused and throw an
exception while trying to render them.

This change makes sure we never expose those broken notifications. For
uniformity, I've applied the same logic to notes and roles mentioned
in notifications, even if nobody reported breakage in those cases.

Tested by creating a few types of notifications with a `notifierId`,
then deleting their user.

(cherry picked from commit 421f8d49e5)

* Update Changelog

* Update CHANGELOG.md

* enhance: 通知がミュートを考慮するようにする

* enhance: 通知が凍結も考慮するようにする

* fix: notifierIdがない通知が消えてしまう問題

* Add tests (通知がミュートを考慮しているかどうか)

* fix: notifierIdがない通知が消えてしまう問題 (grouped)

* Remove unused import

* Fix: typo

* Revert "enhance: 通知が凍結も考慮するようにする"

This reverts commit b1e57e571dfd9a7d8b2430294473c2053cc3ea33.

* Revert API handling

* Remove unused imports

* enhance: Check if notifierId is valid in NotificationEntityService

* 通知作成時にpackしてnullになったらあとの処理をやめる

* Remove duplication of valid notifier check

* add filter notification is not null

* Revert "Remove duplication of valid notifier check"

This reverts commit 239a6952f717add53d52c3e701e7362eb1987645.

* Improve performance

* Fix packGrouped

* Refactor: 判定部分を共通化

* Fix condition

* use isNotNull

* Update CHANGELOG.md

* filterの改善

* Refactor: DONT REPEAT YOURSELF
Note: GroupedNotificationはNotificationの拡張なのでその例外だけ書けば基本的に共通の処理になり複雑な個別の処理は増えにくいと思われる

* Add groupedNotificationTypes

* Update misskey-js typedef

* Refactor: less sql calls

* refactor

* clean up

* filter notes to mark as read

* packed noteがmapなのでそちらを使う

* if (notesToRead.size > 0)

* if (notes.length === 0) return;

* fix

* Revert "if (notes.length === 0) return;"

This reverts commit 22e2324f9633bddba50769ef838bc5ddb4564c88.

* 🎨

* console.error

* err

* remove try-catch

* 不要なジェネリクスを除去

* Revert  (既読処理をpack内で行うものを元に戻す)

* Clean

* Update packages/backend/src/core/entities/NotificationEntityService.ts

* Update packages/backend/src/core/entities/NotificationEntityService.ts

* Update packages/backend/src/core/entities/NotificationEntityService.ts

* Update packages/backend/src/core/entities/NotificationEntityService.ts

* Update packages/backend/src/core/NotificationService.ts

* Clean

---------

Co-authored-by: dakkar <dakkar@thenautilus.net>
Co-authored-by: kakkokari-gtyih <daisho7308+f@gmail.com>
Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-02-28 21:26:26 +09:00
zyoshoka
29350c9f33
refactor(frontend): os.ts周りのリファクタリング (#13186)
* 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>
2024-02-28 18:26:38 +09:00
zyoshoka
664aeb3ced
fix(backend): リノート時のHTLへのストリーミングの意図しない挙動を修正 (#13425)
* fix(backend): リノート時のストリーミングの意図しない挙動を修正

* Update CHANGELOG.md

* fix: 不要な返り値

* fix: 不適切な条件分岐を修正

* test(backend): add htl tests

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-02-28 17:43:17 +09:00
okayurisotto
b7d9d16201
refactor(backend): ノートのエクスポート処理でStreams APIを使うように (#13465)
* 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>
2024-02-28 15:34:58 +09:00
zyoshoka
0d47877db1
enhance(backend): フォロー・フォロワー関連の通知の受信設定の強化 (#13468)
* enhance(backend): 通知の受信設定に「フォロー中またはフォロワー」を追加

* fix(backend): 通知の受信設定で「相互フォロー」が正しく動作しない問題を修正

* Update CHANGELOG.md
2024-02-28 09:49:34 +09:00
zawa-ch
f906ad6ca7
Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 (#13463)
* コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加

* コメント修正
2024-02-27 18:45:46 +09:00
zyoshoka
0fb7b98f96
fix(backend): fix incorrect schemas (#13458) 2024-02-26 19:49:12 +09:00
zyoshoka
0a0af6887a
test(frontend): Chromaticテストが落ちるのを修正 (#13448)
* test(frontend): Chromaticテストが落ちるのを修正

* fix: テストケースを修正

* refactor: comment
2024-02-25 18:06:40 +09:00
FineArchs
dd48366ed8
admin/emoji/updateの必須項目を減らす 等 (#13449)
* admin/emoji/update enhancement

* add CustomEmojiService.getEmojiByName

* update endpoint

* fix

* Update update.ts

* Update autogen files

* type assertion

* Update CHANGELOG.md
2024-02-25 18:06:26 +09:00
tamaina
2c6f25b710
fix: 古いキャッシュを使うのを修正 (#13453) 2024-02-25 12:36:10 +09:00
zyoshoka
792168fdfa
fix(frontend): userActivationがない環境において不具合が生じる問題を修正 (#13451) 2024-02-24 18:06:10 +09:00
syuilo
41747b6ee2 refactor 2024-02-24 11:50:10 +09:00
1Step621
e3dd3f6b63
Enhance(frontend): リアクションピッカーを調整 (#13354)
* 打てない絵文字を表示しないのではなくグレーアウトするように など

* fix: 今度は検索とピン留めに効いてなかった

* lint fix

* use Map

* 斜めに線を引いてわかりやすく

* 斜め線は右上からのほうが良かったかも

* デザイン調整
2024-02-24 10:22:23 +09:00
60 changed files with 2178 additions and 2093 deletions

View file

@ -1,95 +0,0 @@
name: Lint
on:
push:
branches:
- stable
- develop
paths:
- packages/**
pull_request:
paths:
- packages/backend/**
- packages/frontend/**
- packages/sw/**
- packages/misskey-js/**
- packages/shared/.eslintrc.js
jobs:
pnpm_install:
runs-on: docker
steps:
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: https://github.com/pnpm/action-setup@v2
with:
version: 8
run_install: false
- uses: https://code.forgejo.org/actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
lint:
needs: [pnpm_install]
runs-on: docker
continue-on-error: true
strategy:
matrix:
workspace:
- backend
- frontend
- sw
- misskey-js
steps:
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: https://github.com/pnpm/action-setup@v2
with:
version: 7
run_install: false
- uses: https://code.forgejo.org/actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- run: pnpm --filter ${{ matrix.workspace }} run eslint
typecheck:
needs: [pnpm_install]
runs-on: docker
continue-on-error: true
strategy:
matrix:
workspace:
- backend
- misskey-js
steps:
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: https://github.com/pnpm/action-setup@v2
with:
version: 7
run_install: false
- uses: https://code.forgejo.org/actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- run: pnpm --filter misskey-js run build
if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter misskey-reversi run build:tsc
if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter misskey-bubble-game run build
if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter ${{ matrix.workspace }} run typecheck

View file

@ -1,17 +1,19 @@
<!-- ## Unreleased
## 202x.x.x (unreleased)
### General ### General
- -
### Client ### Client
- - Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
- Enhance: リアクション・いいねの総数を表示するように
- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
### Server ### Server
- -
-->
## 2024.3.1 ## 2024.3.1
### General ### General

View file

@ -307,6 +307,98 @@ export const handlers = [
Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files. Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
## Nest
### Nest Service Circular dependency / Nestでサービスの循環参照でエラーが起きた場合
#### forwardRef
まずは簡単に`forwardRef`を試してみる
```typescript
export class FooService {
constructor(
@Inject(forwardRef(() => BarService))
private barService: BarService
) {
}
}
```
#### OnModuleInit
できなければ`OnModuleInit`を使う
```typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { BarService } from '@/core/BarService';
@Injectable()
export class FooService implements OnModuleInit {
private barService: BarService // constructorから移動してくる
constructor(
private moduleRef: ModuleRef,
) {
}
async onModuleInit() {
this.barService = this.moduleRef.get(BarService.name);
}
public async niceMethod() {
return await this.barService.incredibleMethod({ hoge: 'fuga' });
}
}
```
##### Service Unit Test
テストで`onModuleInit`を呼び出す必要がある
```typescript
// import ...
describe('test', () => {
let app: TestingModule;
let fooService: FooService; // for test case
let barService: BarService; // for test case
beforeEach(async () => {
app = await Test.createTestingModule({
imports: ...,
providers: [
FooService,
{ // mockする (mockは必須ではないかもしれない)
provide: BarService,
useFactory: () => ({
incredibleMethod: jest.fn(),
}),
},
{ // Provideにする
provide: BarService.name,
useExisting: BarService,
},
],
})
.useMocker(...
.compile();
fooService = app.get<FooService>(FooService);
barService = app.get<BarService>(BarService) as jest.Mocked<BarService>;
// onModuleInitを実行する
await fooService.onModuleInit();
});
test('nice', () => {
await fooService.niceMethod();
expect(barService.incredibleMethod).toHaveBeenCalled();
expect(barService.incredibleMethod.mock.lastCall![0])
.toEqual({ hoge: 'fuga' });
});
})
```
## Notes ## Notes
### Misskeyのドメイン固有の概念は`Mi`をprefixする ### Misskeyのドメイン固有の概念は`Mi`をprefixする

10
locales/index.d.ts vendored
View file

@ -2044,6 +2044,10 @@ export interface Locale extends ILocale {
* *
*/ */
"showNoteActionsOnlyHover": string; "showNoteActionsOnlyHover": string;
/**
*
*/
"showReactionsCount": string;
/** /**
* *
*/ */
@ -9167,7 +9171,11 @@ export interface Locale extends ILocale {
*/ */
"reactedBySomeUsers": ParameterizedString<"n">; "reactedBySomeUsers": ParameterizedString<"n">;
/** /**
* {n} * {n}
*/
"likedBySomeUsers": ParameterizedString<"n">;
/**
* {n}
*/ */
"renotedBySomeUsers": ParameterizedString<"n">; "renotedBySomeUsers": ParameterizedString<"n">;
/** /**

View file

@ -507,6 +507,7 @@ emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない" disableDrawer: "メニューをドロワーで表示しない"
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する" showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
showReactionsCount: "ノートのリアクション数を表示する"
noHistory: "履歴はありません" noHistory: "履歴はありません"
signinHistory: "ログイン履歴" signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする" enableAdvancedMfm: "高度なMFMを有効にする"
@ -2419,7 +2420,8 @@ _notification:
sendTestNotification: "テスト通知を送信する" sendTestNotification: "テスト通知を送信する"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
reactedBySomeUsers: "{n}人がリアクションしました" reactedBySomeUsers: "{n}人がリアクションしました"
renotedBySomeUsers: "{n}人がブーストしました" likedBySomeUsers: "{n}人がいいねしました"
renotedBySomeUsers: "{n}人がリノートしました"
followedBySomeUsers: "{n}人にフォローされました" followedBySomeUsers: "{n}人にフォローされました"
flushNotification: "通知の履歴をリセットする" flushNotification: "通知の履歴をリセットする"

View file

@ -51,21 +51,35 @@ export class FetchInstanceMetadataService {
} }
@bindThis @bindThis
public async tryLock(host: string): Promise<boolean> { // public for test
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET'); public async tryLock(host: string): Promise<string | null> {
return mutex !== '1'; // TODO: マイグレーションなのであとで消す (2024.3.1)
this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`);
return await this.redisClient.set(
`fetchInstanceMetadata:mutex:v2:${host}`, '1',
'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
'GET' // 古い値を返すなかったらnull
);
} }
@bindThis @bindThis
public unlock(host: string): Promise<'OK'> { // public for test
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0'); public unlock(host: string): Promise<number> {
return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`);
} }
@bindThis @bindThis
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> { public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
const host = instance.host; const host = instance.host;
// Acquire mutex to ensure no parallel runs
if (!await this.tryLock(host)) return; // finallyでunlockされてしまうのでtry内でロックチェックをしない
// returnであってもfinallyは実行される
if (!force && await this.tryLock(host) === '1') {
// 1が返ってきていたらロックされているという意味なので、何もしない
return;
}
try { try {
if (!force) { if (!force) {
const _instance = await this.federatedInstanceService.fetch(host); const _instance = await this.federatedInstanceService.fetch(host);

View file

@ -248,7 +248,7 @@ export class DriveFileEntityService {
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
detail: true, detail: true,
}) : null, }) : null,
userId: opts.withUser ? file.userId : null, userId: file.userId,
user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null, user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
}); });
} }

View file

@ -351,6 +351,7 @@ export class NoteEntityService implements OnModuleInit {
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount, renoteCount: note.renoteCount,
repliesCount: note.repliesCount, repliesCount: note.repliesCount,
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
reactions: this.reactionService.convertLegacyReactions(note.reactions), reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined, reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,

View file

@ -223,6 +223,10 @@ export const packedNoteSchema = {
}], }],
}, },
}, },
reactionCount: {
type: 'number',
optional: false, nullable: false,
},
renoteCount: { renoteCount: {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -86,8 +86,8 @@
//#endregion //#endregion
//#region Script //#region Script
function importAppScript() { async function importAppScript() {
import(`/vite/${CLIENT_ENTRY}`) await import(`/vite/${CLIENT_ENTRY}`)
.catch(async e => { .catch(async e => {
console.error(e); console.error(e);
renderError('APP_IMPORT', e); renderError('APP_IMPORT', e);

View file

@ -187,7 +187,7 @@ describe('2要素認証', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('が設定でき、OTPでログインできる。', async () => { test('が設定でき、OTPでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
@ -197,18 +197,18 @@ describe('2要素認証', () => {
assert.strictEqual(registerResponse.body.label, username); assert.strictEqual(registerResponse.body.label, username);
assert.strictEqual(registerResponse.body.issuer, config.host); assert.strictEqual(registerResponse.body.issuer, config.host);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}, alice); }, alice);
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}); });
@ -216,24 +216,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、セキュリティキーでログインできる。', async () => { test('が設定でき、セキュリティキーでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', { const registerKeyResponse = await api('i/2fa/register-key', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
@ -243,23 +243,23 @@ describe('2要素認証', () => {
const keyName = 'example-key'; const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}), alice); }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName); assert.strictEqual(keyDoneResponse.body.name, keyName);
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, true); assert.strictEqual(usersShowResponse.body.securityKeys, true);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
@ -268,7 +268,7 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.allowCredentials, undefined); assert.notEqual(signinResponse.body.allowCredentials, undefined);
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url')); assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({ const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
keyName, keyName,
credentialId, credentialId,
requestOptions: signinResponse.body, requestOptions: signinResponse.body,
@ -277,24 +277,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => { test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', { const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
}, alice); }, alice);
@ -302,33 +302,33 @@ describe('2要素認証', () => {
const keyName = 'example-key'; const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}), alice); }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
const passwordLessResponse = await api('/i/2fa/password-less', { const passwordLessResponse = await api('i/2fa/password-less', {
value: true, value: true,
}, alice); }, alice);
assert.strictEqual(passwordLessResponse.status, 204); assert.strictEqual(passwordLessResponse.status, 204);
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true); assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
password: '', password: '',
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined); assert.strictEqual(signinResponse.body.i, undefined);
const signinResponse2 = await api('/signin', { const signinResponse2 = await api('signin', {
...signinWithSecurityKeyParam({ ...signinWithSecurityKeyParam({
keyName, keyName,
credentialId, credentialId,
@ -340,24 +340,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => { test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', { const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
}, alice); }, alice);
@ -365,22 +365,22 @@ describe('2要素認証', () => {
const keyName = 'example-key'; const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}), alice); }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
const renamedKey = 'other-key'; const renamedKey = 'other-key';
const updateKeyResponse = await api('/i/2fa/update-key', { const updateKeyResponse = await api('i/2fa/update-key', {
name: renamedKey, name: renamedKey,
credentialId: credentialId.toString('base64url'), credentialId: credentialId.toString('base64url'),
}, alice); }, alice);
assert.strictEqual(updateKeyResponse.status, 200); assert.strictEqual(updateKeyResponse.status, 200);
const iResponse = await api('/i', { const iResponse = await api('i', {
}, alice); }, alice);
assert.strictEqual(iResponse.status, 200); assert.strictEqual(iResponse.status, 200);
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url')); const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
@ -389,24 +389,24 @@ describe('2要素認証', () => {
assert.notEqual(securityKeys[0].lastUsed, undefined); assert.notEqual(securityKeys[0].lastUsed, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、設定したセキュリティキーを削除できる。', async () => { test('が設定でき、設定したセキュリティキーを削除できる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', { const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
}, alice); }, alice);
@ -414,20 +414,20 @@ describe('2要素認証', () => {
const keyName = 'example-key'; const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}), alice); }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す // テストの実行順によっては複数残ってるので全部消す
const iResponse = await api('/i', { const iResponse = await api('i', {
}, alice); }, alice);
assert.strictEqual(iResponse.status, 200); assert.strictEqual(iResponse.status, 200);
for (const key of iResponse.body.securityKeysList) { for (const key of iResponse.body.securityKeysList) {
const removeKeyResponse = await api('/i/2fa/remove-key', { const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
credentialId: key.id, credentialId: key.id,
@ -435,13 +435,13 @@ describe('2要素認証', () => {
assert.strictEqual(removeKeyResponse.status, 200); assert.strictEqual(removeKeyResponse.status, 200);
} }
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, false); assert.strictEqual(usersShowResponse.body.securityKeys, false);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}); });
@ -449,43 +449,43 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => { test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const unregisterResponse = await api('/i/2fa/unregister', { const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
}, alice); }, alice);
assert.strictEqual(unregisterResponse.status, 204); assert.strictEqual(unregisterResponse.status, 204);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);

View file

@ -7,7 +7,6 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { Packed } from '@/misc/json-schema.js';
import { import {
api, api,
failedApiCall, failedApiCall,
@ -29,10 +28,7 @@ describe('アンテナ', () => {
// エンティティとしてのアンテナを主眼においたテストを記述する // エンティティとしてのアンテナを主眼においたテストを記述する
// (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからートを取得するエンドポイントをテストする) // (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからートを取得するエンドポイントをテストする)
// BUG misskey-jsとjson-schemaが一致していない。 type Antenna = misskey.entities.Antenna;
// - srcのenumにgroupが残っている
// - userGroupIdが残っている, isActiveがない
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
type User = misskey.entities.SignupResponse; type User = misskey.entities.SignupResponse;
type Note = misskey.entities.Note; type Note = misskey.entities.Note;
@ -80,7 +76,7 @@ describe('アンテナ', () => {
aliceList = await userList(alice, {}); aliceList = await userList(alice, {});
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
aliceList = await userList(alice, {}); aliceList = await userList(alice, {});
bobFile = (await uploadFile(bob)).body; bobFile = (await uploadFile(bob)).body!;
bobList = await userList(bob); bobList = await userList(bob);
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice); await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice);
@ -129,9 +125,9 @@ describe('アンテナ', () => {
beforeEach(async () => { beforeEach(async () => {
// テスト間で影響し合わないように毎回全部消す。 // テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) { for (const user of [alice, bob]) {
const list = await api('/antennas/list', {}, user); const list = await api('antennas/list', {}, user);
for (const antenna of list.body) { for (const antenna of list.body) {
await api('/antennas/delete', { antennaId: antenna.id }, user); await api('antennas/delete', { antennaId: antenna.id }, user);
} }
} }
}); });
@ -141,11 +137,11 @@ describe('アンテナ', () => {
test('が作成できること、キーが過不足なく入っていること。', async () => { test('が作成できること、キーが過不足なく入っていること。', async () => {
const response = await successfulApiCall({ const response = await successfulApiCall({
endpoint: 'antennas/create', endpoint: 'antennas/create',
parameters: { ...defaultParam }, parameters: defaultParam,
user: alice, user: alice,
}); });
assert.match(response.id, /[0-9a-z]{10}/); assert.match(response.id, /[0-9a-z]{10}/);
const expected = { const expected: Antenna = {
id: response.id, id: response.id,
caseSensitive: false, caseSensitive: false,
createdAt: new Date(response.createdAt).toISOString(), createdAt: new Date(response.createdAt).toISOString(),
@ -161,7 +157,7 @@ describe('アンテナ', () => {
withFile: false, withFile: false,
withReplies: false, withReplies: false,
localOnly: false, localOnly: false,
} as Antenna; };
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
@ -202,27 +198,27 @@ describe('アンテナ', () => {
}); });
const antennaParamPattern = [ const antennaParamPattern = [
{ parameters: (): object => ({ name: 'x'.repeat(100) }) }, { parameters: () => ({ name: 'x'.repeat(100) }) },
{ parameters: (): object => ({ name: 'x' }) }, { parameters: () => ({ name: 'x' }) },
{ parameters: (): object => ({ src: 'home' }) }, { parameters: () => ({ src: 'home' as const }) },
{ parameters: (): object => ({ src: 'all' }) }, { parameters: () => ({ src: 'all' as const }) },
{ parameters: (): object => ({ src: 'users' }) }, { parameters: () => ({ src: 'users' as const }) },
{ parameters: (): object => ({ src: 'list' }) }, { parameters: () => ({ src: 'list' as const }) },
{ parameters: (): object => ({ userListId: null }) }, { parameters: () => ({ userListId: null }) },
{ parameters: (): object => ({ src: 'list', userListId: aliceList.id }) }, { parameters: () => ({ src: 'list' as const, userListId: aliceList.id }) },
{ parameters: (): object => ({ keywords: [['x']] }) }, { parameters: () => ({ keywords: [['x']] }) },
{ parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, { parameters: () => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, { parameters: () => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: (): object => ({ users: [alice.username] }) }, { parameters: () => ({ users: [alice.username] }) },
{ parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) }, { parameters: () => ({ users: [alice.username, bob.username, carol.username] }) },
{ parameters: (): object => ({ caseSensitive: false }) }, { parameters: () => ({ caseSensitive: false }) },
{ parameters: (): object => ({ caseSensitive: true }) }, { parameters: () => ({ caseSensitive: true }) },
{ parameters: (): object => ({ withReplies: false }) }, { parameters: () => ({ withReplies: false }) },
{ parameters: (): object => ({ withReplies: true }) }, { parameters: () => ({ withReplies: true }) },
{ parameters: (): object => ({ withFile: false }) }, { parameters: () => ({ withFile: false }) },
{ parameters: (): object => ({ withFile: true }) }, { parameters: () => ({ withFile: true }) },
{ parameters: (): object => ({ notify: false }) }, { parameters: () => ({ notify: false }) },
{ parameters: (): object => ({ notify: true }) }, { parameters: () => ({ notify: true }) },
]; ];
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => { test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
const response = await successfulApiCall({ const response = await successfulApiCall({
@ -335,7 +331,7 @@ describe('アンテナ', () => {
test.each([ test.each([
{ {
label: '全体から', label: '全体から',
parameters: (): object => ({ src: 'all' }), parameters: () => ({ src: 'all' }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@ -346,7 +342,7 @@ describe('アンテナ', () => {
{ {
// BUG e4144a1 以降home指定は壊れている(allと同じ) // BUG e4144a1 以降home指定は壊れている(allと同じ)
label: 'ホーム指定はallと同じ', label: 'ホーム指定はallと同じ',
parameters: (): object => ({ src: 'home' }), parameters: () => ({ src: 'home' }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@ -357,7 +353,7 @@ describe('アンテナ', () => {
{ {
// https://github.com/misskey-dev/misskey/issues/9025 // https://github.com/misskey-dev/misskey/issues/9025
label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。', label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true }, { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true }, { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
@ -367,56 +363,56 @@ describe('アンテナ', () => {
}, },
{ {
label: 'ブロックしているユーザーのノートは含む', label: 'ブロックしているユーザーのノートは含む',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: 'ブロックされているユーザーのノートは含まない', label: 'ブロックされているユーザーのノートは含まない',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) }, { note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) },
], ],
}, },
{ {
label: 'ミュートしているユーザーのノートは含まない', label: 'ミュートしているユーザーのノートは含まない',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) }, { note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) },
], ],
}, },
{ {
label: 'ミュートされているユーザーのノートは含む', label: 'ミュートされているユーザーのノートは含む',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: '「見つけやすくする」がOFFのユーザーのートも含まれる', label: '「見つけやすくする」がOFFのユーザーのートも含まれる',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: '鍵付きユーザーのノートも含まれる', label: '鍵付きユーザーのノートも含まれる',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: 'サイレンスのノートも含まれる', label: 'サイレンスのノートも含まれる',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: '削除ユーザーのノートも含まれる', label: '削除ユーザーのノートも含まれる',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true },
@ -424,7 +420,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'ユーザー指定で', label: 'ユーザー指定で',
parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }), parameters: () => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) }, { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -433,7 +429,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'リスト指定で', label: 'リスト指定で',
parameters: (): object => ({ src: 'list', userListId: aliceList.id }), parameters: () => ({ src: 'list', userListId: aliceList.id }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) }, { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -442,14 +438,14 @@ describe('アンテナ', () => {
}, },
{ {
label: 'CWにもマッチする', label: 'CWにもマッチする',
parameters: (): object => ({ keywords: [[keyword]] }), parameters: () => ({ keywords: [[keyword]] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true },
], ],
}, },
{ {
label: 'キーワード1つ', label: 'キーワード1つ',
parameters: (): object => ({ keywords: [[keyword]] }), parameters: () => ({ keywords: [[keyword]] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: 'test' }) }, { note: (): Promise<Note> => post(alice, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -458,7 +454,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'キーワード3つ(AND)', label: 'キーワード3つ(AND)',
parameters: (): object => ({ keywords: [['A', 'B', 'C']] }), parameters: () => ({ keywords: [['A', 'B', 'C']] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test A' }) }, { note: (): Promise<Note> => post(bob, { text: 'test A' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A B' }) }, { note: (): Promise<Note> => post(bob, { text: 'test A B' }) },
@ -469,7 +465,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'キーワード3つ(OR)', label: 'キーワード3つ(OR)',
parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }), parameters: () => ({ keywords: [['A'], ['B'], ['C']] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test' }) }, { note: (): Promise<Note> => post(bob, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true }, { note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true },
@ -482,7 +478,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '除外ワード3つ(AND)', label: '除外ワード3つ(AND)',
parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }), parameters: () => ({ excludeKeywords: [['A', 'B', 'C']] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true },
@ -495,7 +491,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '除外ワード3つ(OR)', label: '除外ワード3つ(OR)',
parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }), parameters: () => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) },
@ -508,7 +504,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'キーワード1つ(大文字小文字区別する)', label: 'キーワード1つ(大文字小文字区別する)',
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }), parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: true }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }) }, { note: (): Promise<Note> => post(bob, { text: 'keyword' }) },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) }, { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) },
@ -517,7 +513,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'キーワード1つ(大文字小文字区別しない)', label: 'キーワード1つ(大文字小文字区別しない)',
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }), parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: false }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true }, { note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true }, { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true },
@ -526,7 +522,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '除外ワード1つ(大文字小文字区別する)', label: '除外ワード1つ(大文字小文字区別する)',
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }), parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true },
@ -536,7 +532,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '除外ワード1つ(大文字小文字区別しない)', label: '除外ワード1つ(大文字小文字区別しない)',
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }), parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) }, { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) },
@ -546,7 +542,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '添付ファイルを問わない', label: '添付ファイルを問わない',
parameters: (): object => ({ withFile: false }), parameters: () => ({ withFile: false }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -554,7 +550,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '添付ファイル付きのみ', label: '添付ファイル付きのみ',
parameters: (): object => ({ withFile: true }), parameters: () => ({ withFile: true }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }) }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }) },
@ -562,7 +558,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'リプライ以外', label: 'リプライ以外',
parameters: (): object => ({ withReplies: false }), parameters: () => ({ withReplies: false }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) }, { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -570,7 +566,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'リプライも含む', label: 'リプライも含む',
parameters: (): object => ({ withReplies: true }), parameters: () => ({ withReplies: true }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -633,7 +629,7 @@ describe('アンテナ', () => {
endpoint: 'antennas/notes', endpoint: 'antennas/notes',
parameters: { antennaId: antenna.id, ...paginationParam }, parameters: { antennaId: antenna.id, ...paginationParam },
user: alice, user: alice,
}) as any as Note[]; });
}, offsetBy, 'desc'); }, offsetBy, 'desc');
}); });

View file

@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { api, post, signup } from '../utils.js'; import { UserToken, api, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('API visibility', () => { describe('API visibility', () => {
@ -24,38 +24,38 @@ describe('API visibility', () => {
let target2: misskey.entities.SignupResponse; let target2: misskey.entities.SignupResponse;
/** public-post */ /** public-post */
let pub: any; let pub: misskey.entities.Note;
/** home-post */ /** home-post */
let home: any; let home: misskey.entities.Note;
/** followers-post */ /** followers-post */
let fol: any; let fol: misskey.entities.Note;
/** specified-post */ /** specified-post */
let spe: any; let spe: misskey.entities.Note;
/** public-reply to target's post */ /** public-reply to target's post */
let pubR: any; let pubR: misskey.entities.Note;
/** home-reply to target's post */ /** home-reply to target's post */
let homeR: any; let homeR: misskey.entities.Note;
/** followers-reply to target's post */ /** followers-reply to target's post */
let folR: any; let folR: misskey.entities.Note;
/** specified-reply to target's post */ /** specified-reply to target's post */
let speR: any; let speR: misskey.entities.Note;
/** public-mention to target */ /** public-mention to target */
let pubM: any; let pubM: misskey.entities.Note;
/** home-mention to target */ /** home-mention to target */
let homeM: any; let homeM: misskey.entities.Note;
/** followers-mention to target */ /** followers-mention to target */
let folM: any; let folM: misskey.entities.Note;
/** specified-mention to target */ /** specified-mention to target */
let speM: any; let speM: misskey.entities.Note;
/** reply target post */ /** reply target post */
let tgt: any; let tgt: misskey.entities.Note;
//#endregion //#endregion
const show = async (noteId: any, by: any) => { const show = async (noteId: misskey.entities.Note['id'], by?: UserToken) => {
return await api('/notes/show', { return await api('notes/show', {
noteId, noteId,
}, by); }, by);
}; };
@ -70,7 +70,7 @@ describe('API visibility', () => {
target2 = await signup({ username: 'target2' }); target2 = await signup({ username: 'target2' });
// follow alice <= follower // follow alice <= follower
await api('/following/create', { userId: alice.id }, follower); await api('following/create', { userId: alice.id }, follower);
// normal posts // normal posts
pub = await post(alice, { text: 'x', visibility: 'public' }); pub = await post(alice, { text: 'x', visibility: 'public' });
@ -111,7 +111,7 @@ describe('API visibility', () => {
}); });
test('[show] public-postを未認証が見れる', async () => { test('[show] public-postを未認証が見れる', async () => {
const res = await show(pub.id, null); const res = await show(pub.id);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
@ -132,7 +132,7 @@ describe('API visibility', () => {
}); });
test('[show] home-postを未認証が見れる', async () => { test('[show] home-postを未認証が見れる', async () => {
const res = await show(home.id, null); const res = await show(home.id);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
@ -153,7 +153,7 @@ describe('API visibility', () => {
}); });
test('[show] followers-postを未認証が見れない', async () => { test('[show] followers-postを未認証が見れない', async () => {
const res = await show(fol.id, null); const res = await show(fol.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
@ -179,7 +179,7 @@ describe('API visibility', () => {
}); });
test('[show] specified-postを未認証が見れない', async () => { test('[show] specified-postを未認証が見れない', async () => {
const res = await show(spe.id, null); const res = await show(spe.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
//#endregion //#endregion
@ -207,7 +207,7 @@ describe('API visibility', () => {
}); });
test('[show] public-replyを未認証が見れる', async () => { test('[show] public-replyを未認証が見れる', async () => {
const res = await show(pubR.id, null); const res = await show(pubR.id);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
@ -233,7 +233,7 @@ describe('API visibility', () => {
}); });
test('[show] home-replyを未認証が見れる', async () => { test('[show] home-replyを未認証が見れる', async () => {
const res = await show(homeR.id, null); const res = await show(homeR.id);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
@ -259,7 +259,7 @@ describe('API visibility', () => {
}); });
test('[show] followers-replyを未認証が見れない', async () => { test('[show] followers-replyを未認証が見れない', async () => {
const res = await show(folR.id, null); const res = await show(folR.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
@ -290,7 +290,7 @@ describe('API visibility', () => {
}); });
test('[show] specified-replyを未認証が見れない', async () => { test('[show] specified-replyを未認証が見れない', async () => {
const res = await show(speR.id, null); const res = await show(speR.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
//#endregion //#endregion
@ -318,7 +318,7 @@ describe('API visibility', () => {
}); });
test('[show] public-mentionを未認証が見れる', async () => { test('[show] public-mentionを未認証が見れる', async () => {
const res = await show(pubM.id, null); const res = await show(pubM.id);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
@ -344,7 +344,7 @@ describe('API visibility', () => {
}); });
test('[show] home-mentionを未認証が見れる', async () => { test('[show] home-mentionを未認証が見れる', async () => {
const res = await show(homeM.id, null); const res = await show(homeM.id);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
@ -370,7 +370,7 @@ describe('API visibility', () => {
}); });
test('[show] followers-mentionを未認証が見れない', async () => { test('[show] followers-mentionを未認証が見れない', async () => {
const res = await show(folM.id, null); const res = await show(folM.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
@ -401,28 +401,28 @@ describe('API visibility', () => {
}); });
test('[show] specified-mentionを未認証が見れない', async () => { test('[show] specified-mentionを未認証が見れない', async () => {
const res = await show(speM.id, null); const res = await show(speM.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
//#endregion //#endregion
//#region HTL //#region HTL
test('[HTL] public-post が 自分が見れる', async () => { test('[HTL] public-post が 自分が見れる', async () => {
const res = await api('/notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[HTL] public-post が 非フォロワーから見れない', async () => { test('[HTL] public-post が 非フォロワーから見れない', async () => {
const res = await api('/notes/timeline', { limit: 100 }, other); const res = await api('notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
test('[HTL] followers-post が フォロワーから見れる', async () => { test('[HTL] followers-post が フォロワーから見れる', async () => {
const res = await api('/notes/timeline', { limit: 100 }, follower); const res = await api('notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === fol.id); const notes = res.body.filter((n: any) => n.id === fol.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
@ -431,21 +431,21 @@ describe('API visibility', () => {
//#region RTL //#region RTL
test('[replies] followers-reply が フォロワーから見れる', async () => { test('[replies] followers-reply が フォロワーから見れる', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other); const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target); const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
@ -454,14 +454,14 @@ describe('API visibility', () => {
//#region MTL //#region MTL
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await api('/notes/mentions', { limit: 100 }, target); const res = await api('notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
const res = await api('/notes/mentions', { limit: 100 }, target); const res = await api('notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folM.id); const notes = res.body.filter((n: any) => n.id === folM.id);
assert.strictEqual(notes[0].text, '@target x'); assert.strictEqual(notes[0].text, '@target x');

View file

@ -23,32 +23,32 @@ import type * as misskey from 'misskey-js';
describe('API', () => { describe('API', () => {
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
describe('General validation', () => { describe('General validation', () => {
test('wrong type', async () => { test('wrong type', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
// @ts-expect-error string must be string
string: 42, string: 42,
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('missing require param', async () => { test('missing require param', async () => {
const res = await api('/test', { // @ts-expect-error required is required
const res = await api('test', {
string: 'a', string: 'a',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('invalid misskey:id (empty string)', async () => { test('invalid misskey:id (empty string)', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
id: '', id: '',
}); });
@ -56,7 +56,7 @@ describe('API', () => {
}); });
test('valid misskey:id', async () => { test('valid misskey:id', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
id: '8wvhjghbxu', id: '8wvhjghbxu',
}); });
@ -64,7 +64,7 @@ describe('API', () => {
}); });
test('default value', async () => { test('default value', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
string: 'a', string: 'a',
}); });
@ -73,7 +73,7 @@ describe('API', () => {
}); });
test('can set null even if it has default value', async () => { test('can set null even if it has default value', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
nullableDefault: null, nullableDefault: null,
}); });
@ -82,7 +82,7 @@ describe('API', () => {
}); });
test('cannot set undefined if it has default value', async () => { test('cannot set undefined if it has default value', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
nullableDefault: undefined, nullableDefault: undefined,
}); });
@ -99,14 +99,14 @@ describe('API', () => {
// aliceは管理者、APIを使える // aliceは管理者、APIを使える
await successfulApiCall({ await successfulApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: alice, user: alice,
}); });
// bobは一般ユーザーだからダメ // bobは一般ユーザーだからダメ
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: bob, user: bob,
}, { }, {
@ -117,7 +117,7 @@ describe('API', () => {
// publicアクセスももちろんダメ // publicアクセスももちろんダメ
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: undefined, user: undefined,
}, { }, {
@ -128,7 +128,7 @@ describe('API', () => {
// ごまがしもダメ // ごまがしもダメ
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: 'tsukawasete' }, user: { token: 'tsukawasete' },
}, { }, {
@ -138,13 +138,13 @@ describe('API', () => {
}); });
await successfulApiCall({ await successfulApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: application2 }, user: { token: application2 },
}); });
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: application }, user: { token: application },
}, { }, {
@ -154,7 +154,7 @@ describe('API', () => {
}); });
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: application3 }, user: { token: application3 },
}, { }, {
@ -164,7 +164,7 @@ describe('API', () => {
}); });
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: application4 }, user: { token: application4 },
}, { }, {
@ -177,7 +177,7 @@ describe('API', () => {
describe('Authentication header', () => { describe('Authentication header', () => {
test('一般リクエスト', async () => { test('一般リクエスト', async () => {
await successfulApiCall({ await successfulApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { user: {
token: alice.token, token: alice.token,
@ -211,7 +211,7 @@ describe('API', () => {
describe('tokenエラー応答でWWW-Authenticate headerを送る', () => { describe('tokenエラー応答でWWW-Authenticate headerを送る', () => {
describe('invalid_token', () => { describe('invalid_token', () => {
test('一般リクエスト', async () => { test('一般リクエスト', async () => {
const result = await api('/admin/get-index-stats', {}, { const result = await api('admin/get-index-stats', {}, {
token: 'syuilo', token: 'syuilo',
bearer: true, bearer: true,
}); });
@ -246,7 +246,7 @@ describe('API', () => {
describe('tokenがないとrealmだけおくる', () => { describe('tokenがないとrealmだけおくる', () => {
test('一般リクエスト', async () => { test('一般リクエスト', async () => {
const result = await api('/admin/get-index-stats', {}); const result = await api('admin/get-index-stats', {});
assert.strictEqual(result.status, 401); assert.strictEqual(result.status, 401);
assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"'); assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"');
}); });
@ -259,7 +259,8 @@ describe('API', () => {
}); });
test('invalid_request', async () => { test('invalid_request', async () => {
const result = await api('/notes/create', { text: true }, { // @ts-expect-error text must be string
const result = await api('notes/create', { text: true }, {
token: alice.token, token: alice.token,
bearer: true, bearer: true,
}); });

View file

@ -22,7 +22,7 @@ describe('Block', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('Block作成', async () => { test('Block作成', async () => {
const res = await api('/blocking/create', { const res = await api('blocking/create', {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
@ -30,7 +30,7 @@ describe('Block', () => {
}); });
test('ブロックされているユーザーをフォローできない', async () => { test('ブロックされているユーザーをフォローできない', async () => {
const res = await api('/following/create', { userId: alice.id }, bob); const res = await api('following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
@ -39,7 +39,7 @@ describe('Block', () => {
test('ブロックされているユーザーにリアクションできない', async () => { test('ブロックされているユーザーにリアクションできない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
@ -48,7 +48,7 @@ describe('Block', () => {
test('ブロックされているユーザーに返信できない', async () => { test('ブロックされているユーザーに返信できない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob); const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -57,7 +57,7 @@ describe('Block', () => {
test('ブロックされているユーザーのートをRenoteできない', async () => { test('ブロックされているユーザーのートをRenoteできない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob); const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -72,12 +72,13 @@ describe('Block', () => {
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, bob); const res = await api('notes/local-timeline', {}, bob);
const body = res.body as misskey.entities.Note[];
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); assert.strictEqual(body.some(note => note.id === carolNote.id), true);
}); });
}); });

View file

@ -6,47 +6,34 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { JTDDataType } from 'ajv/dist/jtd';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { Packed } from '@/misc/json-schema.js';
import { paramDef as CreateParamDef } from '@/server/api/endpoints/clips/create.js';
import { paramDef as UpdateParamDef } from '@/server/api/endpoints/clips/update.js';
import { paramDef as DeleteParamDef } from '@/server/api/endpoints/clips/delete.js';
import { paramDef as ShowParamDef } from '@/server/api/endpoints/clips/show.js';
import { paramDef as FavoriteParamDef } from '@/server/api/endpoints/clips/favorite.js';
import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unfavorite.js';
import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js';
import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js';
import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js';
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js'; import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
import type * as Misskey from 'misskey-js';
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
describe('クリップ', () => { describe('クリップ', () => {
type User = Packed<'User'>; let alice: Misskey.entities.SignupResponse;
type Note = Packed<'Note'>; let bob: Misskey.entities.SignupResponse;
type Clip = Packed<'Clip'>; let aliceNote: Misskey.entities.Note;
let aliceHomeNote: Misskey.entities.Note;
let alice: User; let aliceFollowersNote: Misskey.entities.Note;
let bob: User; let aliceSpecifiedNote: Misskey.entities.Note;
let aliceNote: Note; let bobNote: Misskey.entities.Note;
let aliceHomeNote: Note; let bobHomeNote: Misskey.entities.Note;
let aliceFollowersNote: Note; let bobFollowersNote: Misskey.entities.Note;
let aliceSpecifiedNote: Note; let bobSpecifiedNote: Misskey.entities.Note;
let bobNote: Note;
let bobHomeNote: Note;
let bobFollowersNote: Note;
let bobSpecifiedNote: Note;
const compareBy = <T extends { id: string }, >(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { const compareBy = <T extends { id: string }, >(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
return selector(a).localeCompare(selector(b)); return selector(a).localeCompare(selector(b));
}; };
type CreateParam = JTDDataType<typeof CreateParamDef>; const defaultCreate = (): Pick<Misskey.entities.ClipsCreateRequest, 'name'> => ({
const defaultCreate = (): Partial<CreateParam> => ({
name: 'test', name: 'test',
}); });
const create = async (parameters: Partial<CreateParam> = {}, request: Partial<ApiRequest> = {}): Promise<Clip> => { const create = async (parameters: Partial<Misskey.entities.ClipsCreateRequest> = {}, request: Partial<ApiRequest<'clips/create'>> = {}): Promise<Misskey.entities.Clip> => {
const clip = await successfulApiCall<Clip>({ const clip = await successfulApiCall({
endpoint: '/clips/create', endpoint: 'clips/create',
parameters: { parameters: {
...defaultCreate(), ...defaultCreate(),
...parameters, ...parameters,
@ -64,17 +51,16 @@ describe('クリップ', () => {
return clip; return clip;
}; };
const createMany = async (parameters: Partial<CreateParam>, count = 10, user = alice): Promise<Clip[]> => { const createMany = async (parameters: Partial<Misskey.entities.ClipsCreateRequest>, count = 10, user = alice): Promise<Misskey.entities.Clip[]> => {
return await Promise.all([...Array(count)].map((_, i) => create({ return await Promise.all([...Array(count)].map((_, i) => create({
name: `test${i}`, name: `test${i}`,
...parameters, ...parameters,
}, { user }))); }, { user })));
}; };
type UpdateParam = JTDDataType<typeof UpdateParamDef>; const update = async (parameters: Optional<Misskey.entities.ClipsUpdateRequest, 'name'>, request: Partial<ApiRequest<'clips/update'>> = {}): Promise<Misskey.entities.Clip> => {
const update = async (parameters: Partial<UpdateParam>, request: Partial<ApiRequest> = {}): Promise<Clip> => { const clip = await successfulApiCall({
const clip = await successfulApiCall<Clip>({ endpoint: 'clips/update',
endpoint: '/clips/update',
parameters: { parameters: {
name: 'updated', name: 'updated',
...parameters, ...parameters,
@ -92,41 +78,39 @@ describe('クリップ', () => {
return clip; return clip;
}; };
type DeleteParam = JTDDataType<typeof DeleteParamDef>; const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
const deleteClip = async (parameters: DeleteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return await successfulApiCall({
return await successfulApiCall<void>({ endpoint: 'clips/delete',
endpoint: '/clips/delete',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
type ShowParam = JTDDataType<typeof ShowParamDef>; const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
const show = async (parameters: ShowParam, request: Partial<ApiRequest> = {}): Promise<Clip> => { return await successfulApiCall({
return await successfulApiCall<Clip>({ endpoint: 'clips/show',
endpoint: '/clips/show',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}); });
}; };
const list = async (request: Partial<ApiRequest>): Promise<Clip[]> => { const list = async (request: Partial<ApiRequest<'clips/list'>>): Promise<Misskey.entities.Clip[]> => {
return successfulApiCall<Clip[]>({ return successfulApiCall({
endpoint: '/clips/list', endpoint: 'clips/list',
parameters: {}, parameters: {},
user: alice, user: alice,
...request, ...request,
}); });
}; };
const usersClips = async (request: Partial<ApiRequest>): Promise<Clip[]> => { const usersClips = async (parameters: Misskey.entities.UsersClipsRequest, request: Partial<ApiRequest<'users/clips'>> = {}): Promise<Misskey.entities.Clip[]> => {
return await successfulApiCall<Clip[]>({ return await successfulApiCall({
endpoint: '/users/clips', endpoint: 'users/clips',
parameters: {}, parameters,
user: alice, user: alice,
...request, ...request,
}); });
@ -136,23 +120,22 @@ describe('クリップ', () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
// FIXME: misskey-jsのNoteはoutdatedなので直接変換できない aliceNote = await post(alice, { text: 'test' });
aliceNote = await post(alice, { text: 'test' }) as any; aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' });
aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' });
aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' });
aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; bobNote = await post(bob, { text: 'test' });
bobNote = await post(bob, { text: 'test' }) as any; bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' });
bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' });
bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' });
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterEach(async () => { afterEach(async () => {
// テスト間で影響し合わないように毎回全部消す。 // テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) { for (const user of [alice, bob]) {
const list = await api('/clips/list', { limit: 11 }, user); const list = await api('clips/list', { limit: 11 }, user);
for (const clip of list.body) { for (const clip of list.body) {
await api('/clips/delete', { clipId: clip.id }, user); await api('clips/delete', { clipId: clip.id }, user);
} }
} }
}); });
@ -177,7 +160,7 @@ describe('クリップ', () => {
} }
await failedApiCall({ await failedApiCall({
endpoint: '/clips/create', endpoint: 'clips/create',
parameters: defaultCreate(), parameters: defaultCreate(),
user: alice, user: alice,
}, { }, {
@ -204,7 +187,8 @@ describe('クリップ', () => {
{ label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } }, { label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } },
]; ];
test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({ test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({
endpoint: '/clips/create', endpoint: 'clips/create',
// @ts-expect-error invalid params
parameters: { parameters: {
...defaultCreate(), ...defaultCreate(),
...parameters, ...parameters,
@ -246,15 +230,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} }, } },
{ label: '他人のクリップ', user: (): User => bob, assertion: { { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} }, } },
...createClipDenyPattern as any, ...createClipDenyPattern as any,
])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ ])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/update', endpoint: 'clips/update',
parameters: { parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
name: 'updated', name: 'updated',
...parameters, ...parameters,
}, },
@ -279,14 +263,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754', id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} }, } },
{ label: '他人のクリップ', user: (): User => bob, assertion: { { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754', id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} }, } },
])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ ])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/delete', endpoint: 'clips/delete',
parameters: { parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, // @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters, ...parameters,
}, },
user: alice, user: alice,
@ -306,7 +291,7 @@ describe('クリップ', () => {
test('のID指定取得は他人のPrivateなクリップは取得できない', async () => { test('のID指定取得は他人のPrivateなクリップは取得できない', async () => {
const clip = await create({ isPublic: false }, { user: bob } ); const clip = await create({ isPublic: false }, { user: bob } );
failedApiCall({ failedApiCall({
endpoint: '/clips/show', endpoint: 'clips/show',
parameters: { clipId: clip.id }, parameters: { clipId: clip.id },
user: alice, user: alice,
}, { }, {
@ -323,7 +308,8 @@ describe('クリップ', () => {
id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20',
} }, } },
])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({ ])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({
endpoint: '/clips/show', endpoint: 'clips/show',
// @ts-expect-error clipId must not be undefined
parameters: { parameters: {
...parameters, ...parameters,
}, },
@ -356,27 +342,23 @@ describe('クリップ', () => {
test('の一覧が取得できる(空)', async () => { test('の一覧が取得できる(空)', async () => {
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
},
}); });
assert.deepStrictEqual(res, []); assert.deepStrictEqual(res, []);
}); });
test.each([ test.each([
{ label: '' }, { label: '' },
{ label: '他人アカウントから', user: (): User => bob }, { label: '他人アカウントから', user: () => bob },
])('の一覧が$label取得できる', async () => { ])('の一覧が$label取得できる', async () => {
const clips = await createMany({ isPublic: true }); const clips = await createMany({ isPublic: true });
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
},
}); });
// 返ってくる配列には順序保障がないのでidでソートして厳密比較 // 返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual( assert.deepStrictEqual(
res.sort(compareBy<Clip>(s => s.id)), res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
clips.sort(compareBy(s => s.id))); clips.sort(compareBy(s => s.id)));
// 認証状態で見たときだけisFavoritedが入っている // 認証状態で見たときだけisFavoritedが入っている
@ -386,17 +368,16 @@ describe('クリップ', () => {
}); });
test.each([ test.each([
{ label: '未認証', user: (): undefined => undefined }, { label: '未認証', user: () => undefined },
{ label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } }, { label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } },
])('の一覧は$labelでも取得できる', async ({ parameters, user }) => { ])('の一覧は$labelでも取得できる', async ({ parameters, user }) => {
const clips = await createMany({ isPublic: true }); const clips = await createMany({ isPublic: true });
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
limit: clips.length, limit: clips.length,
...parameters, ...parameters,
}, }, {
user: (user ?? ((): User => alice))(), user: (user ?? (() => alice))(),
}); });
// 未認証で見たときはisFavoritedは入らない // 未認証で見たときはisFavoritedは入らない
@ -409,10 +390,8 @@ describe('クリップ', () => {
await create({ isPublic: false }); await create({ isPublic: false });
const aliceClip = await create({ isPublic: true }); const aliceClip = await create({ isPublic: true });
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
limit: 2, limit: 2,
},
}); });
assert.deepStrictEqual(res, [aliceClip]); assert.deepStrictEqual(res, [aliceClip]);
}); });
@ -421,17 +400,15 @@ describe('クリップ', () => {
const clips = await createMany({ isPublic: true }, 7); const clips = await createMany({ isPublic: true }, 7);
clips.sort(compareBy(s => s.id)); clips.sort(compareBy(s => s.id));
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
sinceId: clips[1].id, sinceId: clips[1].id,
untilId: clips[5].id, untilId: clips[5].id,
limit: 4, limit: 4,
},
}); });
// Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較 // Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual( assert.deepStrictEqual(
res.sort(compareBy<Clip>(s => s.id)), res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
[clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない [clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない
clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id)); clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id));
}); });
@ -441,8 +418,9 @@ describe('クリップ', () => {
{ label: 'limitゼロ', parameters: { limit: 0 } }, { label: 'limitゼロ', parameters: { limit: 0 } },
{ label: 'limit最大+1', parameters: { limit: 101 } }, { label: 'limit最大+1', parameters: { limit: 101 } },
])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({ ])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({
endpoint: '/users/clips', endpoint: 'users/clips',
parameters: { parameters: {
// @ts-expect-error userId must not be undefined
userId: alice.id, userId: alice.id,
...parameters, ...parameters,
}, },
@ -454,15 +432,15 @@ describe('クリップ', () => {
})); }));
test.each([ test.each([
{ label: '作成', endpoint: '/clips/create' }, { label: '作成', endpoint: 'clips/create' as const },
{ label: '更新', endpoint: '/clips/update' }, { label: '更新', endpoint: 'clips/update' as const },
{ label: '削除', endpoint: '/clips/delete' }, { label: '削除', endpoint: 'clips/delete' as const },
{ label: '取得', endpoint: '/clips/list' }, { label: '取得', endpoint: 'clips/list' as const },
{ label: 'お気に入り設定', endpoint: '/clips/favorite' }, { label: 'お気に入り設定', endpoint: 'clips/favorite' as const },
{ label: 'お気に入り解除', endpoint: '/clips/unfavorite' }, { label: 'お気に入り解除', endpoint: 'clips/unfavorite' as const },
{ label: 'お気に入り取得', endpoint: '/clips/my-favorites' }, { label: 'お気に入り取得', endpoint: 'clips/my-favorites' as const },
{ label: 'ノート追加', endpoint: '/clips/add-note' }, { label: 'ノート追加', endpoint: 'clips/add-note' as const },
{ label: 'ノート削除', endpoint: '/clips/remove-note' }, { label: 'ノート削除', endpoint: 'clips/remove-note' as const },
])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({ ])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({
endpoint: endpoint, endpoint: endpoint,
parameters: {}, parameters: {},
@ -474,35 +452,33 @@ describe('クリップ', () => {
})); }));
describe('のお気に入り', () => { describe('のお気に入り', () => {
let aliceClip: Clip; let aliceClip: Misskey.entities.Clip;
type FavoriteParam = JTDDataType<typeof FavoriteParamDef>; const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
const favorite = async (parameters: FavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return successfulApiCall({
return successfulApiCall<void>({ endpoint: 'clips/favorite',
endpoint: '/clips/favorite',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
type UnfavoriteParam = JTDDataType<typeof UnfavoriteParamDef>; const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
const unfavorite = async (parameters: UnfavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return successfulApiCall({
return successfulApiCall<void>({ endpoint: 'clips/unfavorite',
endpoint: '/clips/unfavorite',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
const myFavorites = async (request: Partial<ApiRequest> = {}): Promise<Clip[]> => { const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
return successfulApiCall<Clip[]>({ return successfulApiCall({
endpoint: '/clips/my-favorites', endpoint: 'clips/my-favorites',
parameters: {}, parameters: {},
user: alice, user: alice,
...request, ...request,
@ -568,7 +544,7 @@ describe('クリップ', () => {
test('は同じクリップに対して二回設定できない。', async () => { test('は同じクリップに対して二回設定できない。', async () => {
await favorite({ clipId: aliceClip.id }); await favorite({ clipId: aliceClip.id });
await failedApiCall({ await failedApiCall({
endpoint: '/clips/favorite', endpoint: 'clips/favorite',
parameters: { parameters: {
clipId: aliceClip.id, clipId: aliceClip.id,
}, },
@ -586,14 +562,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} }, } },
{ label: '他人のクリップ', user: (): User => bob, assertion: { { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} }, } },
])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ ])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/favorite', endpoint: 'clips/favorite',
parameters: { parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, // @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters, ...parameters,
}, },
user: alice, user: alice,
@ -619,7 +596,7 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '2603966e-b865-426c-94a7-af4a01241dc1', id: '2603966e-b865-426c-94a7-af4a01241dc1',
} }, } },
{ label: '他人のクリップ', user: (): User => bob, assertion: { { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NOT_FAVORITED', code: 'NOT_FAVORITED',
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} }, } },
@ -628,9 +605,10 @@ describe('クリップ', () => {
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} }, } },
])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ ])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/unfavorite', endpoint: 'clips/unfavorite',
parameters: { parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, // @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters, ...parameters,
}, },
user: alice, user: alice,
@ -655,41 +633,38 @@ describe('クリップ', () => {
}); });
describe('に紐づくノート', () => { describe('に紐づくノート', () => {
let aliceClip: Clip; let aliceClip: Misskey.entities.Clip;
const sampleNotes = (): Note[] => [ const sampleNotes = (): Misskey.entities.Note[] => [
aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote,
bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote, bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote,
]; ];
type AddNoteParam = JTDDataType<typeof AddNoteParamDef>; const addNote = async (parameters: Misskey.entities.ClipsAddNoteRequest, request: Partial<ApiRequest<'clips/add-note'>> = {}): Promise<void> => {
const addNote = async (parameters: AddNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return successfulApiCall({
return successfulApiCall<void>({ endpoint: 'clips/add-note',
endpoint: '/clips/add-note',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
type RemoveNoteParam = JTDDataType<typeof RemoveNoteParamDef>; const removeNote = async (parameters: Misskey.entities.ClipsRemoveNoteRequest, request: Partial<ApiRequest<'clips/remove-note'>> = {}): Promise<void> => {
const removeNote = async (parameters: RemoveNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return successfulApiCall({
return successfulApiCall<void>({ endpoint: 'clips/remove-note',
endpoint: '/clips/remove-note',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
type NotesParam = JTDDataType<typeof NotesParamDef>; const notes = async (parameters: Misskey.entities.ClipsNotesRequest, request: Partial<ApiRequest<'clips/notes'>> = {}): Promise<Misskey.entities.Note[]> => {
const notes = async (parameters: Partial<NotesParam>, request: Partial<ApiRequest> = {}): Promise<Note[]> => { return successfulApiCall({
return successfulApiCall<Note[]>({ endpoint: 'clips/notes',
endpoint: '/clips/notes',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
@ -715,7 +690,7 @@ describe('クリップ', () => {
test('として同じノートを二回紐づけることはできない', async () => { test('として同じノートを二回紐づけることはできない', async () => {
await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); await addNote({ clipId: aliceClip.id, noteId: aliceNote.id });
await failedApiCall({ await failedApiCall({
endpoint: '/clips/add-note', endpoint: 'clips/add-note',
parameters: { parameters: {
clipId: aliceClip.id, clipId: aliceClip.id,
noteId: aliceNote.id, noteId: aliceNote.id,
@ -733,11 +708,11 @@ describe('クリップ', () => {
const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1;
const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, {
text: `test ${i}`, text: `test ${i}`,
}) as unknown)) as Note[]; }) as unknown)) as Misskey.entities.Note[];
await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id }))); await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id })));
await failedApiCall({ await failedApiCall({
endpoint: '/clips/add-note', endpoint: 'clips/add-note',
parameters: { parameters: {
clipId: aliceClip.id, clipId: aliceClip.id,
noteId: aliceNote.id, noteId: aliceNote.id,
@ -751,7 +726,7 @@ describe('クリップ', () => {
}); });
test('は他人のクリップへ追加できない。', async () => await failedApiCall({ test('は他人のクリップへ追加できない。', async () => await failedApiCall({
endpoint: '/clips/add-note', endpoint: 'clips/add-note',
parameters: { parameters: {
clipId: aliceClip.id, clipId: aliceClip.id,
noteId: aliceNote.id, noteId: aliceNote.id,
@ -774,18 +749,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE', code: 'NO_SUCH_NOTE',
id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b', id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b',
} }, } },
{ label: '他人のクリップ', user: (): object => bob, assetion: { { label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf',
} }, } },
])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ ])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
endpoint: '/clips/add-note', endpoint: 'clips/add-note',
parameters: { parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id, clipId: aliceClip.id,
// @ts-expect-error noteId must not be undefined
noteId: aliceNote.id, noteId: aliceNote.id,
...parameters, ...parameters,
}, },
user: (user ?? ((): User => alice))(), user: (user ?? (() => alice))(),
}, { }, {
status: 400, status: 400,
code: 'INVALID_PARAM', code: 'INVALID_PARAM',
@ -810,18 +787,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE', code: 'NO_SUCH_NOTE',
id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる
} }, } },
{ label: '他人のクリップ', user: (): object => bob, assetion: { { label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる
} }, } },
])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ ])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
endpoint: '/clips/remove-note', endpoint: 'clips/remove-note',
parameters: { parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id, clipId: aliceClip.id,
// @ts-expect-error noteId must not be undefined
noteId: aliceNote.id, noteId: aliceNote.id,
...parameters, ...parameters,
}, },
user: (user ?? ((): User => alice))(), user: (user ?? (() => alice))(),
}, { }, {
status: 400, status: 400,
code: 'INVALID_PARAM', code: 'INVALID_PARAM',
@ -925,21 +904,22 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} }, } },
{ label: '他人のPrivateなクリップから', user: (): object => bob, assertion: { { label: '他人のPrivateなクリップから', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} }, } },
{ label: '未認証でPrivateなクリップから', user: (): undefined => undefined, assertion: { { label: '未認証でPrivateなクリップから', user: () => undefined, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} }, } },
])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({ ])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/notes', endpoint: 'clips/notes',
parameters: { parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id, clipId: aliceClip.id,
...parameters, ...parameters,
}, },
user: (user ?? ((): User => alice))(), user: (user ?? (() => alice))(),
}, { }, {
status: 400, status: 400,
code: 'INVALID_PARAM', code: 'INVALID_PARAM',

View file

@ -6,22 +6,14 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { MiNote } from '@/models/Note.js'; import { api, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import { api, initTestDb, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
import type{ Repository } from 'typeorm'
import type { Packed } from '@/misc/json-schema.js';
describe('Drive', () => { describe('Drive', () => {
let Notes: Repository<MiNote>;
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
@ -31,13 +23,13 @@ describe('Drive', () => {
const marker = Math.random().toString(); const marker = Math.random().toString();
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg' const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg';
const catcher = makeStreamCatcher( const catcher = makeStreamCatcher(
alice, alice,
'main', 'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>, (msg) => msg.body.file,
10 * 1000); 10 * 1000);
const res = await api('drive/files/upload-from-url', { const res = await api('drive/files/upload-from-url', {
@ -51,7 +43,7 @@ describe('Drive', () => {
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
assert.strictEqual(file.name, 'Lenna.jpg'); assert.strictEqual(file.name, 'Lenna.jpg');
assert.strictEqual(file.type, 'image/jpeg'); assert.strictEqual(file.type, 'image/jpeg');
}) });
test('ローカルからアップロードできる', async () => { test('ローカルからアップロードできる', async () => {
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする // APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
@ -59,27 +51,27 @@ describe('Drive', () => {
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' }); const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' });
assert.strictEqual(res.body?.name, 'テスト画像.jpg'); assert.strictEqual(res.body?.name, 'テスト画像.jpg');
assert.strictEqual(res.body?.type, 'image/jpeg'); assert.strictEqual(res.body.type, 'image/jpeg');
}) });
test('添付ノート一覧を取得できる', async () => { test('添付ノート一覧を取得できる', async () => {
const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id) const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id);
const note0 = await post(alice, { fileIds: [ids[0]] }); const note0 = await post(alice, { fileIds: [ids[0]] });
const note1 = await post(alice, { fileIds: [ids[0], ids[1]] }); const note1 = await post(alice, { fileIds: [ids[0], ids[1]] });
const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice); const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice);
assert.strictEqual(attached0.body.length, 2); assert.strictEqual(attached0.body.length, 2);
assert.strictEqual(attached0.body[0].id, note1.id) assert.strictEqual(attached0.body[0].id, note1.id);
assert.strictEqual(attached0.body[1].id, note0.id) assert.strictEqual(attached0.body[1].id, note0.id);
const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice); const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice);
assert.strictEqual(attached1.body.length, 1); assert.strictEqual(attached1.body.length, 1);
assert.strictEqual(attached1.body[0].id, note1.id) assert.strictEqual(attached1.body[0].id, note1.id);
const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice); const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice);
assert.strictEqual(attached2.body.length, 0) assert.strictEqual(attached2.body.length, 0);
}) });
test('添付ノート一覧は他の人から見えない', async () => { test('添付ノート一覧は他の人から見えない', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
@ -89,7 +81,5 @@ describe('Drive', () => {
const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob); const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual('error' in res.body, true); assert.strictEqual('error' in res.body, true);
});
})
}); });

View file

@ -79,6 +79,7 @@ describe('Endpoints', () => {
test('クエリをインジェクションできない', async () => { test('クエリをインジェクションできない', async () => {
const res = await api('signin', { const res = await api('signin', {
username: 'test1', username: 'test1',
// @ts-expect-error password must be string
password: { password: {
$gt: '', $gt: '',
}, },
@ -103,7 +104,7 @@ describe('Endpoints', () => {
const myLocation = '七森中'; const myLocation = '七森中';
const myBirthday = '2000-09-07'; const myBirthday = '2000-09-07';
const res = await api('/i/update', { const res = await api('i/update', {
name: myName, name: myName,
location: myLocation, location: myLocation,
birthday: myBirthday, birthday: myBirthday,
@ -117,7 +118,7 @@ describe('Endpoints', () => {
}); });
test('名前を空白にできる', async () => { test('名前を空白にできる', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
name: ' ', name: ' ',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -125,11 +126,11 @@ describe('Endpoints', () => {
}); });
test('誕生日の設定を削除できる', async () => { test('誕生日の設定を削除できる', async () => {
await api('/i/update', { await api('i/update', {
birthday: '2000-09-07', birthday: '2000-09-07',
}, alice); }, alice);
const res = await api('/i/update', { const res = await api('i/update', {
birthday: null, birthday: null,
}, alice); }, alice);
@ -139,7 +140,7 @@ describe('Endpoints', () => {
}); });
test('不正な誕生日の形式で怒られる', async () => { test('不正な誕生日の形式で怒られる', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
birthday: '2000/09/07', birthday: '2000/09/07',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
@ -148,7 +149,7 @@ describe('Endpoints', () => {
describe('users/show', () => { describe('users/show', () => {
test('ユーザーが取得できる', async () => { test('ユーザーが取得できる', async () => {
const res = await api('/users/show', { const res = await api('users/show', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -158,14 +159,14 @@ describe('Endpoints', () => {
}); });
test('ユーザーが存在しなかったら怒る', async () => { test('ユーザーが存在しなかったら怒る', async () => {
const res = await api('/users/show', { const res = await api('users/show', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
}); });
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/users/show', { const res = await api('users/show', {
userId: 'kyoppie', userId: 'kyoppie',
}); });
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
@ -178,7 +179,7 @@ describe('Endpoints', () => {
text: 'test', text: 'test',
}); });
const res = await api('/notes/show', { const res = await api('notes/show', {
noteId: myPost.id, noteId: myPost.id,
}, alice); }, alice);
@ -189,14 +190,14 @@ describe('Endpoints', () => {
}); });
test('投稿が存在しなかったら怒る', async () => { test('投稿が存在しなかったら怒る', async () => {
const res = await api('/notes/show', { const res = await api('notes/show', {
noteId: '000000000000000000000000', noteId: '000000000000000000000000',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/notes/show', { const res = await api('notes/show', {
noteId: 'kyoppie', noteId: 'kyoppie',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
@ -207,14 +208,14 @@ describe('Endpoints', () => {
test('リアクションできる', async () => { test('リアクションできる', async () => {
const bobPost = await post(bob, { text: 'hi' }); const bobPost = await post(bob, { text: 'hi' });
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: bobPost.id, noteId: bobPost.id,
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const resNote = await api('/notes/show', { const resNote = await api('notes/show', {
noteId: bobPost.id, noteId: bobPost.id,
}, alice); }, alice);
@ -225,7 +226,7 @@ describe('Endpoints', () => {
test('自分の投稿にもリアクションできる', async () => { test('自分の投稿にもリアクションできる', async () => {
const myPost = await post(alice, { text: 'hi' }); const myPost = await post(alice, { text: 'hi' });
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: myPost.id, noteId: myPost.id,
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
@ -236,19 +237,19 @@ describe('Endpoints', () => {
test('二重にリアクションすると上書きされる', async () => { test('二重にリアクションすると上書きされる', async () => {
const bobPost = await post(bob, { text: 'hi' }); const bobPost = await post(bob, { text: 'hi' });
await api('/notes/reactions/create', { await api('notes/reactions/create', {
noteId: bobPost.id, noteId: bobPost.id,
reaction: '🥰', reaction: '🥰',
}, alice); }, alice);
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: bobPost.id, noteId: bobPost.id,
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const resNote = await api('/notes/show', { const resNote = await api('notes/show', {
noteId: bobPost.id, noteId: bobPost.id,
}, alice); }, alice);
@ -257,7 +258,7 @@ describe('Endpoints', () => {
}); });
test('存在しない投稿にはリアクションできない', async () => { test('存在しない投稿にはリアクションできない', async () => {
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: '000000000000000000000000', noteId: '000000000000000000000000',
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
@ -266,13 +267,14 @@ describe('Endpoints', () => {
}); });
test('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
const res = await api('/notes/reactions/create', {}, alice); // @ts-expect-error param must not be empty
const res = await api('notes/reactions/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: 'kyoppie', noteId: 'kyoppie',
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
@ -283,7 +285,7 @@ describe('Endpoints', () => {
describe('following/create', () => { describe('following/create', () => {
test('フォローできる', async () => { test('フォローできる', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -301,7 +303,7 @@ describe('Endpoints', () => {
}); });
test('既にフォローしている場合は怒る', async () => { test('既にフォローしている場合は怒る', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -309,7 +311,7 @@ describe('Endpoints', () => {
}); });
test('存在しないユーザーはフォローできない', async () => { test('存在しないユーザーはフォローできない', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
}, alice); }, alice);
@ -317,7 +319,7 @@ describe('Endpoints', () => {
}); });
test('自分自身はフォローできない', async () => { test('自分自身はフォローできない', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -325,13 +327,14 @@ describe('Endpoints', () => {
}); });
test('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
const res = await api('/following/create', {}, alice); // @ts-expect-error params must not be empty
const res = await api('following/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: 'foo', userId: 'foo',
}, alice); }, alice);
@ -341,11 +344,11 @@ describe('Endpoints', () => {
describe('following/delete', () => { describe('following/delete', () => {
test('フォロー解除できる', async () => { test('フォロー解除できる', async () => {
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -363,7 +366,7 @@ describe('Endpoints', () => {
}); });
test('フォローしていない場合は怒る', async () => { test('フォローしていない場合は怒る', async () => {
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -371,7 +374,7 @@ describe('Endpoints', () => {
}); });
test('存在しないユーザーはフォロー解除できない', async () => { test('存在しないユーザーはフォロー解除できない', async () => {
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
}, alice); }, alice);
@ -379,7 +382,7 @@ describe('Endpoints', () => {
}); });
test('自分自身はフォロー解除できない', async () => { test('自分自身はフォロー解除できない', async () => {
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -387,13 +390,14 @@ describe('Endpoints', () => {
}); });
test('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
const res = await api('/following/delete', {}, alice); // @ts-expect-error params must not be empty
const res = await api('following/delete', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: 'kyoppie', userId: 'kyoppie',
}, alice); }, alice);
@ -403,20 +407,20 @@ describe('Endpoints', () => {
describe('channels/search', () => { describe('channels/search', () => {
test('空白検索で一覧を取得できる', async () => { test('空白検索で一覧を取得できる', async () => {
await api('/channels/create', { await api('channels/create', {
name: 'aaa', name: 'aaa',
description: 'bbb', description: 'bbb',
}, bob); }, bob);
await api('/channels/create', { await api('channels/create', {
name: 'ccc1', name: 'ccc1',
description: 'ddd1', description: 'ddd1',
}, bob); }, bob);
await api('/channels/create', { await api('channels/create', {
name: 'ccc2', name: 'ccc2',
description: 'ddd2', description: 'ddd2',
}, bob); }, bob);
const res = await api('/channels/search', { const res = await api('channels/search', {
query: '', query: '',
}, bob); }, bob);
@ -425,7 +429,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 3); assert.strictEqual(res.body.length, 3);
}); });
test('名前のみの検索で名前を検索できる', async () => { test('名前のみの検索で名前を検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'aaa', query: 'aaa',
type: 'nameOnly', type: 'nameOnly',
}, bob); }, bob);
@ -436,7 +440,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'aaa'); assert.strictEqual(res.body[0].name, 'aaa');
}); });
test('名前のみの検索で名前を複数検索できる', async () => { test('名前のみの検索で名前を複数検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ccc', query: 'ccc',
type: 'nameOnly', type: 'nameOnly',
}, bob); }, bob);
@ -446,7 +450,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2); assert.strictEqual(res.body.length, 2);
}); });
test('名前のみの検索で説明は検索できない', async () => { test('名前のみの検索で説明は検索できない', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'bbb', query: 'bbb',
type: 'nameOnly', type: 'nameOnly',
}, bob); }, bob);
@ -456,7 +460,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 0); assert.strictEqual(res.body.length, 0);
}); });
test('名前と説明の検索で名前を検索できる', async () => { test('名前と説明の検索で名前を検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ccc1', query: 'ccc1',
}, bob); }, bob);
@ -466,7 +470,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1'); assert.strictEqual(res.body[0].name, 'ccc1');
}); });
test('名前と説明での検索で説明を検索できる', async () => { test('名前と説明での検索で説明を検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ddd1', query: 'ddd1',
}, bob); }, bob);
@ -476,7 +480,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1'); assert.strictEqual(res.body[0].name, 'ccc1');
}); });
test('名前と説明の検索で名前を複数検索できる', async () => { test('名前と説明の検索で名前を複数検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ccc', query: 'ccc',
}, bob); }, bob);
@ -485,7 +489,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2); assert.strictEqual(res.body.length, 2);
}); });
test('名前と説明での検索で説明を複数検索できる', async () => { test('名前と説明での検索で説明を複数検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ddd', query: 'ddd',
}, bob); }, bob);
@ -506,7 +510,7 @@ describe('Endpoints', () => {
await uploadFile(alice, { await uploadFile(alice, {
blob: new Blob([new Uint8Array(1024)]), blob: new Blob([new Uint8Array(1024)]),
}); });
const res = await api('/drive', {}, alice); const res = await api('drive', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).toHaveProperty('usage', 1792); expect(res.body).toHaveProperty('usage', 1792);
@ -519,7 +523,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Lenna.jpg'); assert.strictEqual(res.body!.name, 'Lenna.jpg');
}); });
test('ファイルに名前を付けられる', async () => { test('ファイルに名前を付けられる', async () => {
@ -527,7 +531,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Belmond.jpg'); assert.strictEqual(res.body!.name, 'Belmond.jpg');
}); });
test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => { test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => {
@ -535,11 +539,12 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Belmond.png.jpg'); assert.strictEqual(res.body!.name, 'Belmond.png.jpg');
}); });
test('ファイル無しで怒られる', async () => { test('ファイル無しで怒られる', async () => {
const res = await api('/drive/files/create', {}, alice); // @ts-expect-error params must not be empty
const res = await api('drive/files/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -549,14 +554,14 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'image.svg'); assert.strictEqual(res.body!.name, 'image.svg');
assert.strictEqual(res.body.type, 'image/svg+xml'); assert.strictEqual(res.body!.type, 'image/svg+xml');
}); });
for (const type of ['webp', 'avif']) { for (const type of ['webp', 'avif']) {
const mediaType = `image/${type}`; const mediaType = `image/${type}`;
const getWebpublicType = async (user: any, fileId: string): Promise<string> => { const getWebpublicType = async (user: misskey.entities.SignupResponse, fileId: string): Promise<string> => {
// drive/files/create does not expose webpublicType directly, so get it by posting it // drive/files/create does not expose webpublicType directly, so get it by posting it
const res = await post(user, { const res = await post(user, {
text: mediaType, text: mediaType,
@ -573,10 +578,10 @@ describe('Endpoints', () => {
const res = await uploadFile(alice, { path }); const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, path); assert.strictEqual(res.body!.name, path);
assert.strictEqual(res.body.type, mediaType); assert.strictEqual(res.body!.type, mediaType);
const webpublicType = await getWebpublicType(alice, res.body.id); const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp'); assert.strictEqual(webpublicType, 'image/webp');
}); });
@ -584,10 +589,10 @@ describe('Endpoints', () => {
const path = `without-alpha.${type}`; const path = `without-alpha.${type}`;
const res = await uploadFile(alice, { path }); const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, path); assert.strictEqual(res.body!.name, path);
assert.strictEqual(res.body.type, mediaType); assert.strictEqual(res.body!.type, mediaType);
const webpublicType = await getWebpublicType(alice, res.body.id); const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp'); assert.strictEqual(webpublicType, 'image/webp');
}); });
} }
@ -598,8 +603,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const newName = 'いちごパスタ.png'; const newName = 'いちごパスタ.png';
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
name: newName, name: newName,
}, alice); }, alice);
@ -611,8 +616,8 @@ describe('Endpoints', () => {
test('他人のファイルは更新できない', async () => { test('他人のファイルは更新できない', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
name: 'いちごパスタ.png', name: 'いちごパスタ.png',
}, bob); }, bob);
@ -621,12 +626,12 @@ describe('Endpoints', () => {
test('親フォルダを更新できる', async () => { test('親フォルダを更新できる', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: folder.id, folderId: folder.id,
}, alice); }, alice);
@ -638,17 +643,17 @@ describe('Endpoints', () => {
test('親フォルダを無しにできる', async () => { test('親フォルダを無しにできる', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
await api('/drive/files/update', { await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: folder.id, folderId: folder.id,
}, alice); }, alice);
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: null, folderId: null,
}, alice); }, alice);
@ -659,12 +664,12 @@ describe('Endpoints', () => {
test('他人のフォルダには入れられない', async () => { test('他人のフォルダには入れられない', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, bob)).body; }, bob)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: folder.id, folderId: folder.id,
}, alice); }, alice);
@ -674,8 +679,8 @@ describe('Endpoints', () => {
test('存在しないフォルダで怒られる', async () => { test('存在しないフォルダで怒られる', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: '000000000000000000000000', folderId: '000000000000000000000000',
}, alice); }, alice);
@ -685,8 +690,8 @@ describe('Endpoints', () => {
test('不正なフォルダIDで怒られる', async () => { test('不正なフォルダIDで怒られる', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: 'foo', folderId: 'foo',
}, alice); }, alice);
@ -694,7 +699,7 @@ describe('Endpoints', () => {
}); });
test('ファイルが存在しなかったら怒る', async () => { test('ファイルが存在しなかったら怒る', async () => {
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: '000000000000000000000000', fileId: '000000000000000000000000',
name: 'いちごパスタ.png', name: 'いちごパスタ.png',
}, alice); }, alice);
@ -706,8 +711,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const newName = ''; const newName = '';
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
name: newName, name: newName,
}, alice); }, alice);
@ -715,7 +720,7 @@ describe('Endpoints', () => {
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: 'kyoppie', fileId: 'kyoppie',
name: 'いちごパスタ.png', name: 'いちごパスタ.png',
}, alice); }, alice);
@ -726,7 +731,7 @@ describe('Endpoints', () => {
describe('drive/folders/create', () => { describe('drive/folders/create', () => {
test('フォルダを作成できる', async () => { test('フォルダを作成できる', async () => {
const res = await api('/drive/folders/create', { const res = await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice); }, alice);
@ -738,11 +743,11 @@ describe('Endpoints', () => {
describe('drive/folders/update', () => { describe('drive/folders/update', () => {
test('名前を更新できる', async () => { test('名前を更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
name: 'new name', name: 'new name',
}, alice); }, alice);
@ -753,11 +758,11 @@ describe('Endpoints', () => {
}); });
test('他人のフォルダを更新できない', async () => { test('他人のフォルダを更新できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, bob)).body; }, bob)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
name: 'new name', name: 'new name',
}, alice); }, alice);
@ -766,14 +771,14 @@ describe('Endpoints', () => {
}); });
test('親フォルダを更新できる', async () => { test('親フォルダを更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('drive/folders/create', {
name: 'parent', name: 'parent',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id, parentId: parentFolder.id,
}, alice); }, alice);
@ -784,18 +789,18 @@ describe('Endpoints', () => {
}); });
test('親フォルダを無しに更新できる', async () => { test('親フォルダを無しに更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('drive/folders/create', {
name: 'parent', name: 'parent',
}, alice)).body; }, alice)).body;
await api('/drive/folders/update', { await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id, parentId: parentFolder.id,
}, alice); }, alice);
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: null, parentId: null,
}, alice); }, alice);
@ -806,14 +811,14 @@ describe('Endpoints', () => {
}); });
test('他人のフォルダを親フォルダに設定できない', async () => { test('他人のフォルダを親フォルダに設定できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('drive/folders/create', {
name: 'parent', name: 'parent',
}, bob)).body; }, bob)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id, parentId: parentFolder.id,
}, alice); }, alice);
@ -822,18 +827,18 @@ describe('Endpoints', () => {
}); });
test('フォルダが循環するような構造にできない', async () => { test('フォルダが循環するような構造にできない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('drive/folders/create', {
name: 'parent', name: 'parent',
}, alice)).body; }, alice)).body;
await api('/drive/folders/update', { await api('drive/folders/update', {
folderId: parentFolder.id, folderId: parentFolder.id,
parentId: folder.id, parentId: folder.id,
}, alice); }, alice);
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id, parentId: parentFolder.id,
}, alice); }, alice);
@ -842,25 +847,25 @@ describe('Endpoints', () => {
}); });
test('フォルダが循環するような構造にできない(再帰的)', async () => { test('フォルダが循環するような構造にできない(再帰的)', async () => {
const folderA = (await api('/drive/folders/create', { const folderA = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const folderB = (await api('/drive/folders/create', { const folderB = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const folderC = (await api('/drive/folders/create', { const folderC = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
await api('/drive/folders/update', { await api('drive/folders/update', {
folderId: folderB.id, folderId: folderB.id,
parentId: folderA.id, parentId: folderA.id,
}, alice); }, alice);
await api('/drive/folders/update', { await api('drive/folders/update', {
folderId: folderC.id, folderId: folderC.id,
parentId: folderB.id, parentId: folderB.id,
}, alice); }, alice);
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folderA.id, folderId: folderA.id,
parentId: folderC.id, parentId: folderC.id,
}, alice); }, alice);
@ -869,11 +874,11 @@ describe('Endpoints', () => {
}); });
test('フォルダが循環するような構造にできない(自身)', async () => { test('フォルダが循環するような構造にできない(自身)', async () => {
const folderA = (await api('/drive/folders/create', { const folderA = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folderA.id, folderId: folderA.id,
parentId: folderA.id, parentId: folderA.id,
}, alice); }, alice);
@ -882,11 +887,11 @@ describe('Endpoints', () => {
}); });
test('存在しない親フォルダを設定できない', async () => { test('存在しない親フォルダを設定できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: '000000000000000000000000', parentId: '000000000000000000000000',
}, alice); }, alice);
@ -895,11 +900,11 @@ describe('Endpoints', () => {
}); });
test('不正な親フォルダIDで怒られる', async () => { test('不正な親フォルダIDで怒られる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: 'foo', parentId: 'foo',
}, alice); }, alice);
@ -908,7 +913,7 @@ describe('Endpoints', () => {
}); });
test('存在しないフォルダを更新できない', async () => { test('存在しないフォルダを更新できない', async () => {
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: '000000000000000000000000', folderId: '000000000000000000000000',
}, alice); }, alice);
@ -916,7 +921,7 @@ describe('Endpoints', () => {
}); });
test('不正なフォルダIDで怒られる', async () => { test('不正なフォルダIDで怒られる', async () => {
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: 'foo', folderId: 'foo',
}, alice); }, alice);
@ -937,7 +942,7 @@ describe('Endpoints', () => {
visibleUserIds: [alice.id], visibleUserIds: [alice.id],
}); });
const res = await api('/notes/replies', { const res = await api('notes/replies', {
noteId: alicePost.id, noteId: alicePost.id,
}, carol); }, carol);
@ -949,7 +954,7 @@ describe('Endpoints', () => {
describe('notes/timeline', () => { describe('notes/timeline', () => {
test('フォロワー限定投稿が含まれる', async () => { test('フォロワー限定投稿が含まれる', async () => {
await api('/following/create', { await api('following/create', {
userId: carol.id, userId: carol.id,
}, dave); }, dave);
@ -958,7 +963,7 @@ describe('Endpoints', () => {
visibility: 'followers', visibility: 'followers',
}); });
const res = await api('/notes/timeline', {}, dave); const res = await api('notes/timeline', {}, dave);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -979,12 +984,12 @@ describe('Endpoints', () => {
test('他者に関するメモを更新できる', async () => { test('他者に関するメモを更新できる', async () => {
const memo = '10月まで低浮上とのこと。'; const memo = '10月まで低浮上とのこと。';
const res1 = await api('/users/update-memo', { const res1 = await api('users/update-memo', {
memo, memo,
userId: bob.id, userId: bob.id,
}, alice); }, alice);
const res2 = await api('/users/show', { const res2 = await api('users/show', {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
assert.strictEqual(res1.status, 204); assert.strictEqual(res1.status, 204);
@ -994,12 +999,12 @@ describe('Endpoints', () => {
test('自分に関するメモを更新できる', async () => { test('自分に関するメモを更新できる', async () => {
const memo = 'チケットを月末までに買う。'; const memo = 'チケットを月末までに買う。';
const res1 = await api('/users/update-memo', { const res1 = await api('users/update-memo', {
memo, memo,
userId: alice.id, userId: alice.id,
}, alice); }, alice);
const res2 = await api('/users/show', { const res2 = await api('users/show', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(res1.status, 204); assert.strictEqual(res1.status, 204);
@ -1009,17 +1014,17 @@ describe('Endpoints', () => {
test('メモを削除できる', async () => { test('メモを削除できる', async () => {
const memo = '10月まで低浮上とのこと。'; const memo = '10月まで低浮上とのこと。';
await api('/users/update-memo', { await api('users/update-memo', {
memo, memo,
userId: bob.id, userId: bob.id,
}, alice); }, alice);
await api('/users/update-memo', { await api('users/update-memo', {
memo: '', memo: '',
userId: bob.id, userId: bob.id,
}, alice); }, alice);
const res = await api('/users/show', { const res = await api('users/show', {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
@ -1032,21 +1037,21 @@ describe('Endpoints', () => {
const memoCarolToBob = '例の件について今度問いただす。'; const memoCarolToBob = '例の件について今度問いただす。';
await Promise.all([ await Promise.all([
api('/users/update-memo', { api('users/update-memo', {
memo: memoAliceToBob, memo: memoAliceToBob,
userId: bob.id, userId: bob.id,
}, alice), }, alice),
api('/users/update-memo', { api('users/update-memo', {
memo: memoCarolToBob, memo: memoCarolToBob,
userId: bob.id, userId: bob.id,
}, carol), }, carol),
]); ]);
const [resAlice, resCarol] = await Promise.all([ const [resAlice, resCarol] = await Promise.all([
api('/users/show', { api('users/show', {
userId: bob.id, userId: bob.id,
}, alice), }, alice),
api('/users/show', { api('users/show', {
userId: bob.id, userId: bob.id,
}, carol), }, carol),
]); ]);

View file

@ -18,7 +18,7 @@ describe('export-clips', () => {
// XXX: Any better way to get the result? // XXX: Any better way to get the result?
async function pollFirstDriveFile() { async function pollFirstDriveFile() {
while (true) { while (true) {
const files = (await api('/drive/files', {}, alice)).body; const files = (await api('drive/files', {}, alice)).body;
if (!files.length) { if (!files.length) {
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));
continue; continue;
@ -26,7 +26,7 @@ describe('export-clips', () => {
if (files.length > 1) { if (files.length > 1) {
throw new Error('Too many files?'); throw new Error('Too many files?');
} }
const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body; const file = (await api('drive/files/show', { fileId: files[0].id }, alice)).body;
const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`)); const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`));
return await res.json(); return await res.json();
} }
@ -44,16 +44,16 @@ describe('export-clips', () => {
beforeEach(async () => { beforeEach(async () => {
// Clean all clips and files of alice // Clean all clips and files of alice
const clips = (await api('/clips/list', {}, alice)).body; const clips = (await api('clips/list', {}, alice)).body;
for (const clip of clips) { for (const clip of clips) {
const res = await api('/clips/delete', { clipId: clip.id }, alice); const res = await api('clips/delete', { clipId: clip.id }, alice);
if (res.status !== 204) { if (res.status !== 204) {
throw new Error('Failed to delete clip'); throw new Error('Failed to delete clip');
} }
} }
const files = (await api('/drive/files', {}, alice)).body; const files = (await api('drive/files', {}, alice)).body;
for (const file of files) { for (const file of files) {
const res = await api('/drive/files/delete', { fileId: file.id }, alice); const res = await api('drive/files/delete', { fileId: file.id }, alice);
if (res.status !== 204) { if (res.status !== 204) {
throw new Error('Failed to delete file'); throw new Error('Failed to delete file');
} }
@ -61,13 +61,13 @@ describe('export-clips', () => {
}); });
test('basic export', async () => { test('basic export', async () => {
let res = await api('/clips/create', { let res = await api('clips/create', {
name: 'foo', name: 'foo',
description: 'bar', description: 'bar',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
res = await api('/i/export-clips', {}, alice); res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
@ -77,7 +77,7 @@ describe('export-clips', () => {
}); });
test('export with notes', async () => { test('export with notes', async () => {
let res = await api('/clips/create', { let res = await api('clips/create', {
name: 'foo', name: 'foo',
description: 'bar', description: 'bar',
}, alice); }, alice);
@ -96,14 +96,14 @@ describe('export-clips', () => {
}); });
for (const note of [note1, note2]) { for (const note of [note1, note2]) {
res = await api('/clips/add-note', { res = await api('clips/add-note', {
clipId: clip.id, clipId: clip.id,
noteId: note.id, noteId: note.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
} }
res = await api('/i/export-clips', {}, alice); res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
@ -116,14 +116,14 @@ describe('export-clips', () => {
}); });
test('multiple clips', async () => { test('multiple clips', async () => {
let res = await api('/clips/create', { let res = await api('clips/create', {
name: 'kawaii', name: 'kawaii',
description: 'kawaii', description: 'kawaii',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const clip1 = res.body; const clip1 = res.body;
res = await api('/clips/create', { res = await api('clips/create', {
name: 'yuri', name: 'yuri',
description: 'yuri', description: 'yuri',
}, alice); }, alice);
@ -138,19 +138,19 @@ describe('export-clips', () => {
text: 'baz2', text: 'baz2',
}); });
res = await api('/clips/add-note', { res = await api('clips/add-note', {
clipId: clip1.id, clipId: clip1.id,
noteId: note1.id, noteId: note1.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
res = await api('/clips/add-note', { res = await api('clips/add-note', {
clipId: clip2.id, clipId: clip2.id,
noteId: note2.id, noteId: note2.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice); res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
@ -163,7 +163,7 @@ describe('export-clips', () => {
}); });
test('Clipping other user\'s note', async () => { test('Clipping other user\'s note', async () => {
let res = await api('/clips/create', { let res = await api('clips/create', {
name: 'kawaii', name: 'kawaii',
description: 'kawaii', description: 'kawaii',
}, alice); }, alice);
@ -175,13 +175,13 @@ describe('export-clips', () => {
visibility: 'followers', visibility: 'followers',
}); });
res = await api('/clips/add-note', { res = await api('clips/add-note', {
clipId: clip.id, clipId: clip.id,
noteId: note.id, noteId: note.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice); res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();

View file

@ -23,13 +23,13 @@ const JSON_UTF8 = 'application/json; charset=utf-8';
describe('Webリソース', () => { describe('Webリソース', () => {
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let aliceUploadedFile: any; let aliceUploadedFile: misskey.entities.DriveFile | null;
let alicesPost: any; let alicesPost: misskey.entities.Note;
let alicePage: any; let alicePage: misskey.entities.Page;
let alicePlay: any; let alicePlay: misskey.entities.Flash;
let aliceClip: any; let aliceClip: misskey.entities.Clip;
let aliceGalleryPost: any; let aliceGalleryPost: misskey.entities.GalleryPost;
let aliceChannel: any; let aliceChannel: misskey.entities.Channel;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
@ -77,7 +77,7 @@ describe('Webリソース', () => {
beforeAll(async () => { beforeAll(async () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
aliceUploadedFile = await uploadFile(alice); aliceUploadedFile = (await uploadFile(alice)).body;
alicesPost = await post(alice, { alicesPost = await post(alice, {
text: 'test', text: 'test',
}); });
@ -85,7 +85,7 @@ describe('Webリソース', () => {
alicePlay = await play(alice, {}); alicePlay = await play(alice, {});
aliceClip = await clip(alice, {}); aliceClip = await clip(alice, {});
aliceGalleryPost = await galleryPost(alice, { aliceGalleryPost = await galleryPost(alice, {
fileIds: [aliceUploadedFile.body.id], fileIds: [aliceUploadedFile!.id],
}); });
aliceChannel = await channel(alice, {}); aliceChannel = await channel(alice, {});

View file

@ -19,15 +19,15 @@ describe('FF visibility', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -39,36 +39,36 @@ describe('FF visibility', () => {
test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => { test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
@ -78,36 +78,36 @@ describe('FF visibility', () => {
test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => { test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
@ -116,15 +116,15 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -136,36 +136,36 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
@ -175,36 +175,36 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
@ -213,15 +213,15 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -231,34 +231,34 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => { test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
@ -267,34 +267,34 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => { test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
@ -302,19 +302,19 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -326,45 +326,45 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => { test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
@ -374,45 +374,45 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => { test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
@ -421,15 +421,15 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => { test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -441,36 +441,36 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
@ -480,36 +480,36 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
@ -518,15 +518,15 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => { test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -536,34 +536,34 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => { test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
@ -572,34 +572,34 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => { test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
@ -609,7 +609,7 @@ describe('FF visibility', () => {
describe('AP', () => { describe('AP', () => {
test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => { test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
}, alice); }, alice);
@ -617,7 +617,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
}, alice); }, alice);
@ -625,7 +625,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 403); assert.strictEqual(followingRes.status, 403);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
}, alice); }, alice);
@ -636,7 +636,7 @@ describe('FF visibility', () => {
test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => { test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
@ -644,7 +644,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
} }
{ {
await api('/i/update', { await api('i/update', {
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
@ -652,7 +652,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 403); assert.strictEqual(followersRes.status, 403);
} }
{ {
await api('/i/update', { await api('i/update', {
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);

View file

@ -55,7 +55,7 @@ describe('Account Move', () => {
}, 1000 * 10); }, 1000 * 10);
test('Able to create an alias', async () => { test('Able to create an alias', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`],
}, bob); }, bob);
@ -67,7 +67,7 @@ describe('Account Move', () => {
}); });
test('Able to create a local alias without hostname', async () => { test('Able to create a local alias without hostname', async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: ['@alice'], alsoKnownAs: ['@alice'],
}, bob); }, bob);
@ -77,7 +77,7 @@ describe('Account Move', () => {
}); });
test('Able to create a local alias without @', async () => { test('Able to create a local alias without @', async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: ['alice'], alsoKnownAs: ['alice'],
}, bob); }, bob);
@ -87,7 +87,7 @@ describe('Account Move', () => {
}); });
test('Able to set remote user (but may fail)', async () => { test('Able to set remote user (but may fail)', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: ['@syuilo@example.com'], alsoKnownAs: ['@syuilo@example.com'],
}, bob); }, bob);
@ -97,7 +97,7 @@ describe('Account Move', () => {
}); });
test('Unable to add duplicated aliases to alsoKnownAs', async () => { test('Unable to add duplicated aliases to alsoKnownAs', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`],
}, bob); }, bob);
@ -107,7 +107,7 @@ describe('Account Move', () => {
}); });
test('Unable to add itself', async () => { test('Unable to add itself', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: [`@bob@${url.hostname}`], alsoKnownAs: [`@bob@${url.hostname}`],
}, bob); }, bob);
@ -117,7 +117,7 @@ describe('Account Move', () => {
}); });
test('Unable to add a nonexisting local account to alsoKnownAs', async () => { test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
const res1 = await api('/i/update', { const res1 = await api('i/update', {
alsoKnownAs: [`@nonexist@${url.hostname}`], alsoKnownAs: [`@nonexist@${url.hostname}`],
}, bob); }, bob);
@ -125,7 +125,7 @@ describe('Account Move', () => {
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER'); assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
const res2 = await api('/i/update', { const res2 = await api('i/update', {
alsoKnownAs: ['@alice', 'nonexist'], alsoKnownAs: ['@alice', 'nonexist'],
}, bob); }, bob);
@ -135,7 +135,7 @@ describe('Account Move', () => {
}); });
test('Able to add two existing local account to alsoKnownAs', async () => { test('Able to add two existing local account to alsoKnownAs', async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`],
}, bob); }, bob);
@ -146,10 +146,10 @@ describe('Account Move', () => {
}); });
test('Able to properly overwrite alsoKnownAs', async () => { test('Able to properly overwrite alsoKnownAs', async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`],
}, bob); }, bob);
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`], alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`],
}, bob); }, bob);
@ -164,27 +164,27 @@ describe('Account Move', () => {
let antennaId = ''; let antennaId = '';
beforeAll(async () => { beforeAll(async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`],
}, root); }, root);
const listRoot = await api('/users/lists/create', { const listRoot = await api('users/lists/create', {
name: secureRndstr(8), name: secureRndstr(8),
}, root); }, root);
await api('/users/lists/push', { await api('users/lists/push', {
listId: listRoot.body.id, listId: listRoot.body.id,
userId: alice.id, userId: alice.id,
}, root); }, root);
await api('/following/create', { await api('following/create', {
userId: root.id, userId: root.id,
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: eve.id, userId: eve.id,
}, alice); }, alice);
const antenna = await api('/antennas/create', { const antenna = await api('antennas/create', {
name: secureRndstr(8), name: secureRndstr(8),
src: 'home', src: 'home',
keywords: [secureRndstr(8)], keywords: [[secureRndstr(8)]],
excludeKeywords: [], excludeKeywords: [],
users: [], users: [],
caseSensitive: false, caseSensitive: false,
@ -195,48 +195,48 @@ describe('Account Move', () => {
}, alice); }, alice);
antennaId = antenna.body.id; antennaId = antenna.body.id;
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`],
}, bob); }, bob);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, carol); }, carol);
await api('/mute/create', { await api('mute/create', {
userId: alice.id, userId: alice.id,
}, dave); }, dave);
await api('/blocking/create', { await api('blocking/create', {
userId: alice.id, userId: alice.id,
}, dave); }, dave);
await api('/following/create', { await api('following/create', {
userId: eve.id, userId: eve.id,
}, dave); }, dave);
await api('/following/create', { await api('following/create', {
userId: dave.id, userId: dave.id,
}, eve); }, eve);
const listEve = await api('/users/lists/create', { const listEve = await api('users/lists/create', {
name: secureRndstr(8), name: secureRndstr(8),
}, eve); }, eve);
await api('/users/lists/push', { await api('users/lists/push', {
listId: listEve.body.id, listId: listEve.body.id,
userId: bob.id, userId: bob.id,
}, eve); }, eve);
await api('/i/update', { await api('i/update', {
isLocked: true, isLocked: true,
}, frank); }, frank);
await api('/following/create', { await api('following/create', {
userId: frank.id, userId: frank.id,
}, alice); }, alice);
await api('/following/requests/accept', { await api('following/requests/accept', {
userId: alice.id, userId: alice.id,
}, frank); }, frank);
}, 1000 * 10); }, 1000 * 10);
test('Prohibit the root account from moving', async () => { test('Prohibit the root account from moving', async () => {
const res = await api('/i/move', { const res = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`, moveToAccount: `@bob@${url.hostname}`,
}, root); }, root);
@ -246,7 +246,7 @@ describe('Account Move', () => {
}); });
test('Unable to move to a nonexisting local account', async () => { test('Unable to move to a nonexisting local account', async () => {
const res = await api('/i/move', { const res = await api('i/move', {
moveToAccount: `@nonexist@${url.hostname}`, moveToAccount: `@nonexist@${url.hostname}`,
}, alice); }, alice);
@ -256,7 +256,7 @@ describe('Account Move', () => {
}); });
test('Unable to move if alsoKnownAs is invalid', async () => { test('Unable to move if alsoKnownAs is invalid', async () => {
const res = await api('/i/move', { const res = await api('i/move', {
moveToAccount: `@carol@${url.hostname}`, moveToAccount: `@carol@${url.hostname}`,
}, alice); }, alice);
@ -266,7 +266,7 @@ describe('Account Move', () => {
}); });
test('Relationships have been properly migrated', async () => { test('Relationships have been properly migrated', async () => {
const move = await api('/i/move', { const move = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`, moveToAccount: `@bob@${url.hostname}`,
}, alice); }, alice);
@ -275,13 +275,13 @@ describe('Account Move', () => {
await sleep(1000 * 3); // wait for jobs to finish await sleep(1000 * 3); // wait for jobs to finish
// Unfollow delayed? // Unfollow delayed?
const aliceFollowings = await api('/users/following', { const aliceFollowings = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(aliceFollowings.status, 200); assert.strictEqual(aliceFollowings.status, 200);
assert.strictEqual(aliceFollowings.body.length, 3); assert.strictEqual(aliceFollowings.body.length, 3);
const carolFollowings = await api('/users/following', { const carolFollowings = await api('users/following', {
userId: carol.id, userId: carol.id,
}, carol); }, carol);
assert.strictEqual(carolFollowings.status, 200); assert.strictEqual(carolFollowings.status, 200);
@ -289,25 +289,25 @@ describe('Account Move', () => {
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id); assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id); assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
const blockings = await api('/blocking/list', {}, dave); const blockings = await api('blocking/list', {}, dave);
assert.strictEqual(blockings.status, 200); assert.strictEqual(blockings.status, 200);
assert.strictEqual(blockings.body.length, 2); assert.strictEqual(blockings.body.length, 2);
assert.strictEqual(blockings.body[0].blockeeId, bob.id); assert.strictEqual(blockings.body[0].blockeeId, bob.id);
assert.strictEqual(blockings.body[1].blockeeId, alice.id); assert.strictEqual(blockings.body[1].blockeeId, alice.id);
const mutings = await api('/mute/list', {}, dave); const mutings = await api('mute/list', {}, dave);
assert.strictEqual(mutings.status, 200); assert.strictEqual(mutings.status, 200);
assert.strictEqual(mutings.body.length, 2); assert.strictEqual(mutings.body.length, 2);
assert.strictEqual(mutings.body[0].muteeId, bob.id); assert.strictEqual(mutings.body[0].muteeId, bob.id);
assert.strictEqual(mutings.body[1].muteeId, alice.id); assert.strictEqual(mutings.body[1].muteeId, alice.id);
const rootLists = await api('/users/lists/list', {}, root); const rootLists = await api('users/lists/list', {}, root);
assert.strictEqual(rootLists.status, 200); assert.strictEqual(rootLists.status, 200);
assert.strictEqual(rootLists.body[0].userIds.length, 2); assert.strictEqual(rootLists.body[0].userIds.length, 2);
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id)); assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id)); assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
const eveLists = await api('/users/lists/list', {}, eve); const eveLists = await api('users/lists/list', {}, eve);
assert.strictEqual(eveLists.status, 200); assert.strictEqual(eveLists.status, 200);
assert.strictEqual(eveLists.body[0].userIds.length, 1); assert.strictEqual(eveLists.body[0].userIds.length, 1);
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id)); assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
@ -315,13 +315,13 @@ describe('Account Move', () => {
test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => { test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => {
await successfulApiCall({ await successfulApiCall({
endpoint: '/following/create', endpoint: 'following/create',
parameters: { parameters: {
userId: frank.id, userId: frank.id,
}, },
user: bob, user: bob,
}); });
const followers = await api('/users/followers', { const followers = await api('users/followers', {
userId: frank.id, userId: frank.id,
}, frank); }, frank);
@ -333,7 +333,7 @@ describe('Account Move', () => {
test('Unfollowed after 10 sec (24 hours in production).', async () => { test('Unfollowed after 10 sec (24 hours in production).', async () => {
await sleep(1000 * 8); await sleep(1000 * 8);
const following = await api('/users/following', { const following = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -342,7 +342,7 @@ describe('Account Move', () => {
}); });
test('Unable to move if the destination account has already moved.', async () => { test('Unable to move if the destination account has already moved.', async () => {
const res = await api('/i/move', { const res = await api('i/move', {
moveToAccount: `@alice@${url.hostname}`, moveToAccount: `@alice@${url.hostname}`,
}, bob); }, bob);
@ -352,7 +352,7 @@ describe('Account Move', () => {
}); });
test('Follow and follower counts are properly adjusted', async () => { test('Follow and follower counts are properly adjusted', async () => {
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, eve); }, eve);
const newAlice = await Users.findOneByOrFail({ id: alice.id }); const newAlice = await Users.findOneByOrFail({ id: alice.id });
@ -365,7 +365,7 @@ describe('Account Move', () => {
assert.strictEqual(newEve.followingCount, 1); assert.strictEqual(newEve.followingCount, 1);
assert.strictEqual(newEve.followersCount, 1); assert.strictEqual(newEve.followersCount, 1);
await api('/following/delete', { await api('following/delete', {
userId: alice.id, userId: alice.id,
}, eve); }, eve);
newEve = await Users.findOneByOrFail({ id: eve.id }); newEve = await Users.findOneByOrFail({ id: eve.id });
@ -374,49 +374,49 @@ describe('Account Move', () => {
}); });
test.each([ test.each([
'/antennas/create', 'antennas/create',
'/channels/create', 'channels/create',
'/channels/favorite', 'channels/favorite',
'/channels/follow', 'channels/follow',
'/channels/unfavorite', 'channels/unfavorite',
'/channels/unfollow', 'channels/unfollow',
'/clips/add-note', 'clips/add-note',
'/clips/create', 'clips/create',
'/clips/favorite', 'clips/favorite',
'/clips/remove-note', 'clips/remove-note',
'/clips/unfavorite', 'clips/unfavorite',
'/clips/update', 'clips/update',
'/drive/files/upload-from-url', 'drive/files/upload-from-url',
'/flash/create', 'flash/create',
'/flash/like', 'flash/like',
'/flash/unlike', 'flash/unlike',
'/flash/update', 'flash/update',
'/following/create', 'following/create',
'/gallery/posts/create', 'gallery/posts/create',
'/gallery/posts/like', 'gallery/posts/like',
'/gallery/posts/unlike', 'gallery/posts/unlike',
'/gallery/posts/update', 'gallery/posts/update',
'/i/claim-achievement', 'i/claim-achievement',
'/i/move', 'i/move',
'/i/import-blocking', 'i/import-blocking',
'/i/import-following', 'i/import-following',
'/i/import-muting', 'i/import-muting',
'/i/import-user-lists', 'i/import-user-lists',
'/i/pin', 'i/pin',
'/mute/create', 'mute/create',
'/notes/create', 'notes/create',
'/notes/favorites/create', 'notes/favorites/create',
'/notes/polls/vote', 'notes/polls/vote',
'/notes/reactions/create', 'notes/reactions/create',
'/pages/create', 'pages/create',
'/pages/like', 'pages/like',
'/pages/unlike', 'pages/unlike',
'/pages/update', 'pages/update',
'/renote-mute/create', 'renote-mute/create',
'/users/lists/create', 'users/lists/create',
'/users/lists/pull', 'users/lists/pull',
'/users/lists/push', 'users/lists/push',
])('Prohibit access after moving: %s', async (endpoint) => { ] as const)('Prohibit access after moving: %s', async (endpoint) => {
const res = await api(endpoint, {}, alice); const res = await api(endpoint, {}, alice);
assert.strictEqual(res.status, 403); assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
@ -424,11 +424,11 @@ describe('Account Move', () => {
}); });
test('Prohibit access after moving: /antennas/update', async () => { test('Prohibit access after moving: /antennas/update', async () => {
const res = await api('/antennas/update', { const res = await api('antennas/update', {
antennaId, antennaId,
name: secureRndstr(8), name: secureRndstr(8),
src: 'users', src: 'users',
keywords: [secureRndstr(8)], keywords: [[secureRndstr(8)]],
excludeKeywords: [], excludeKeywords: [],
users: [eve.id], users: [eve.id],
caseSensitive: false, caseSensitive: false,
@ -447,12 +447,12 @@ describe('Account Move', () => {
const res = await uploadFile(alice); const res = await uploadFile(alice);
assert.strictEqual(res.status, 403); assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
}); });
test('Prohibit updating alsoKnownAs after moving', async () => { test('Prohibit updating alsoKnownAs after moving', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: [`@eve@${url.hostname}`], alsoKnownAs: [`@eve@${url.hostname}`],
}, alice); }, alice);

View file

@ -19,21 +19,31 @@ describe('Mute', () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
// Mute: alice ==> carol
await api('mute/create', {
userId: carol.id,
}, alice);
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('ミュート作成', async () => { test('ミュート作成', async () => {
const res = await api('/mute/create', { const res = await api('mute/create', {
userId: carol.id, userId: bob.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
// 単体でも走らせられるように副作用消す
await api('mute/delete', {
userId: bob.id,
}, alice);
}); });
test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => { test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice hi' }); const bobNote = await post(bob, { text: '@alice hi' });
const carolNote = await post(carol, { text: '@alice hi' }); const carolNote = await post(carol, { text: '@alice hi' });
const res = await api('/notes/mentions', {}, alice); const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -43,11 +53,11 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
await post(carol, { text: '@alice hi' }); await post(carol, { text: '@alice hi' });
const res = await api('/i', {}, alice); const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false); assert.strictEqual(res.body.hasUnreadMentions, false);
@ -55,7 +65,7 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
@ -64,8 +74,8 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
await api('/notifications/mark-all-as-read', {}, alice); await api('notifications/mark-all-as-read', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
@ -78,7 +88,7 @@ describe('Mute', () => {
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -94,7 +104,7 @@ describe('Mute', () => {
renoteId: carolNote.id, renoteId: carolNote.id,
}); });
const res = await api('/notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -110,7 +120,7 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like'); await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like'); await react(carol, aliceNote, 'like');
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -123,7 +133,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi', replyId: aliceNote.id }); await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id }); await post(carol, { text: '@alice hi', replyId: aliceNote.id });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -137,7 +147,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' }); await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' }); await post(carol, { text: '@alice hi' });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -151,7 +161,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id }); await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id }); await post(carol, { text: 'hi', renoteId: aliceNote.id });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -165,7 +175,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id }); await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id }); await post(carol, { renoteId: aliceNote.id });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -175,30 +185,36 @@ describe('Mute', () => {
}); });
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
await api('/i/follow', { userId: alice.id }, bob); await api('following/create', { userId: alice.id }, bob);
await api('/i/follow', { userId: alice.id }, carol); await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
}); });
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => { test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
await api('/i/update/', { isLocked: true }, alice); await api('i/update', { isLocked: true }, alice);
await api('/following/create', { userId: alice.id }, bob); await api('following/create', { userId: alice.id }, bob);
await api('/following/create', { userId: alice.id }, carol); await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
}); });
}); });
@ -208,7 +224,7 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like'); await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like'); await react(carol, aliceNote, 'like');
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -220,7 +236,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi', replyId: aliceNote.id }); await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id }); await post(carol, { text: '@alice hi', replyId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -234,7 +250,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' }); await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' }); await post(carol, { text: '@alice hi' });
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -248,7 +264,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id }); await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id }); await post(carol, { text: 'hi', renoteId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -262,7 +278,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id }); await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id }); await post(carol, { renoteId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -272,24 +288,27 @@ describe('Mute', () => {
}); });
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
await api('/i/follow', { userId: alice.id }, bob); await api('following/create', { userId: alice.id }, bob);
await api('/i/follow', { userId: alice.id }, carol); await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
}); });
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => { test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
await api('/i/update/', { isLocked: true }, alice); await api('i/update', { isLocked: true }, alice);
await api('/following/create', { userId: alice.id }, bob); await api('following/create', { userId: alice.id }, bob);
await api('/following/create', { userId: alice.id }, carol); await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);

View file

@ -31,7 +31,7 @@ describe('Note', () => {
text: 'test', text: 'test',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -41,7 +41,7 @@ describe('Note', () => {
test('ファイルを添付できる', async () => { test('ファイルを添付できる', async () => {
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await api('/notes/create', { const res = await api('notes/create', {
fileIds: [file.id], fileIds: [file.id],
}, alice); }, alice);
@ -53,7 +53,7 @@ describe('Note', () => {
test('他人のファイルで怒られる', async () => { test('他人のファイルで怒られる', async () => {
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await api('/notes/create', { const res = await api('notes/create', {
text: 'test', text: 'test',
fileIds: [file.id], fileIds: [file.id],
}, alice); }, alice);
@ -64,7 +64,7 @@ describe('Note', () => {
}, 1000 * 10); }, 1000 * 10);
test('存在しないファイルで怒られる', async () => { test('存在しないファイルで怒られる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
text: 'test', text: 'test',
fileIds: ['000000000000000000000000'], fileIds: ['000000000000000000000000'],
}, alice); }, alice);
@ -75,7 +75,7 @@ describe('Note', () => {
}); });
test('不正なファイルIDで怒られる', async () => { test('不正なファイルIDで怒られる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
fileIds: ['kyoppie'], fileIds: ['kyoppie'],
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
@ -93,7 +93,7 @@ describe('Note', () => {
replyId: bobPost.id, replyId: bobPost.id,
}; };
const res = await api('/notes/create', alicePost, alice); const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -111,7 +111,7 @@ describe('Note', () => {
renoteId: bobPost.id, renoteId: bobPost.id,
}; };
const res = await api('/notes/create', alicePost, alice); const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -129,7 +129,7 @@ describe('Note', () => {
renoteId: bobPost.id, renoteId: bobPost.id,
}; };
const res = await api('/notes/create', alicePost, alice); const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -142,7 +142,7 @@ describe('Note', () => {
const bobPost = await post(bob, { const bobPost = await post(bob, {
text: 'test', text: 'test',
}); });
const res = await api('/notes/create', { const res = await api('notes/create', {
text: ' ', text: ' ',
renoteId: bobPost.id, renoteId: bobPost.id,
}, alice); }, alice);
@ -152,7 +152,7 @@ describe('Note', () => {
}); });
test('visibility: followersでrenoteできる', async () => { test('visibility: followersでrenoteできる', async () => {
const createRes = await api('/notes/create', { const createRes = await api('notes/create', {
text: 'test', text: 'test',
visibility: 'followers', visibility: 'followers',
}, alice); }, alice);
@ -160,7 +160,7 @@ describe('Note', () => {
assert.strictEqual(createRes.status, 200); assert.strictEqual(createRes.status, 200);
const renoteId = createRes.body.createdNote.id; const renoteId = createRes.body.createdNote.id;
const renoteRes = await api('/notes/create', { const renoteRes = await api('notes/create', {
visibility: 'followers', visibility: 'followers',
renoteId, renoteId,
}, alice); }, alice);
@ -169,7 +169,7 @@ describe('Note', () => {
assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId); assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId);
assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers'); assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers');
const deleteRes = await api('/notes/delete', { const deleteRes = await api('notes/delete', {
noteId: renoteRes.body.createdNote.id, noteId: renoteRes.body.createdNote.id,
}, alice); }, alice);
@ -177,11 +177,11 @@ describe('Note', () => {
}); });
test('visibility: followersなートに対してフォロワーはリプライできる', async () => { test('visibility: followersなートに対してフォロワーはリプライできる', async () => {
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const aliceNote = await api('/notes/create', { const aliceNote = await api('notes/create', {
text: 'direct note to bob', text: 'direct note to bob',
visibility: 'followers', visibility: 'followers',
}, alice); }, alice);
@ -189,7 +189,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200); assert.strictEqual(aliceNote.status, 200);
const replyId = aliceNote.body.createdNote.id; const replyId = aliceNote.body.createdNote.id;
const bobReply = await api('/notes/create', { const bobReply = await api('notes/create', {
text: 'reply to alice note', text: 'reply to alice note',
replyId, replyId,
}, bob); }, bob);
@ -197,20 +197,20 @@ describe('Note', () => {
assert.strictEqual(bobReply.status, 200); assert.strictEqual(bobReply.status, 200);
assert.strictEqual(bobReply.body.createdNote.replyId, replyId); assert.strictEqual(bobReply.body.createdNote.replyId, replyId);
await api('/following/delete', { await api('following/delete', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
}); });
test('visibility: followersなートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => { test('visibility: followersなートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => {
const aliceNote = await api('/notes/create', { const aliceNote = await api('notes/create', {
text: 'direct note to bob', text: 'direct note to bob',
visibility: 'followers', visibility: 'followers',
}, alice); }, alice);
assert.strictEqual(aliceNote.status, 200); assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', { const bobReply = await api('notes/create', {
text: 'reply to alice note', text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id, replyId: aliceNote.body.createdNote.id,
}, bob); }, bob);
@ -220,7 +220,7 @@ describe('Note', () => {
}); });
test('visibility: specifiedなートに対してvisibility: specifiedで返信できる', async () => { test('visibility: specifiedなートに対してvisibility: specifiedで返信できる', async () => {
const aliceNote = await api('/notes/create', { const aliceNote = await api('notes/create', {
text: 'direct note to bob', text: 'direct note to bob',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [bob.id], visibleUserIds: [bob.id],
@ -228,7 +228,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200); assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', { const bobReply = await api('notes/create', {
text: 'reply to alice note', text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id, replyId: aliceNote.body.createdNote.id,
visibility: 'specified', visibility: 'specified',
@ -239,7 +239,7 @@ describe('Note', () => {
}); });
test('visibility: specifiedなートに対してvisibility: follwersで返信しようとすると怒られる', async () => { test('visibility: specifiedなートに対してvisibility: follwersで返信しようとすると怒られる', async () => {
const aliceNote = await api('/notes/create', { const aliceNote = await api('notes/create', {
text: 'direct note to bob', text: 'direct note to bob',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [bob.id], visibleUserIds: [bob.id],
@ -247,7 +247,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200); assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', { const bobReply = await api('notes/create', {
text: 'reply to alice note with visibility: followers', text: 'reply to alice note with visibility: followers',
replyId: aliceNote.body.createdNote.id, replyId: aliceNote.body.createdNote.id,
visibility: 'followers', visibility: 'followers',
@ -261,7 +261,7 @@ describe('Note', () => {
const post = { const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字 text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
}); });
@ -269,7 +269,7 @@ describe('Note', () => {
const post = { const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字 text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -278,7 +278,7 @@ describe('Note', () => {
text: 'test', text: 'test',
replyId: '000000000000000000000000', replyId: '000000000000000000000000',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -286,7 +286,7 @@ describe('Note', () => {
const post = { const post = {
renoteId: '000000000000000000000000', renoteId: '000000000000000000000000',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -295,7 +295,7 @@ describe('Note', () => {
text: 'test', text: 'test',
replyId: 'foo', replyId: 'foo',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -303,7 +303,7 @@ describe('Note', () => {
const post = { const post = {
renoteId: 'foo', renoteId: 'foo',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -312,7 +312,7 @@ describe('Note', () => {
text: '@ghost yo', text: '@ghost yo',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -324,7 +324,7 @@ describe('Note', () => {
text: '@bob @bob @bob yo', text: '@bob @bob @bob yo',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -337,25 +337,25 @@ describe('Note', () => {
describe('添付ファイル情報', () => { describe('添付ファイル情報', () => {
test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const res = await api('/notes/create', { const res = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.files.length, 1); assert.strictEqual(res.body.createdNote.files.length, 1);
assert.strictEqual(res.body.createdNote.files[0].id, file.body.id); assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
}); });
test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const createdNote = await api('/notes/create', { const createdNote = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(createdNote.status, 200); assert.strictEqual(createdNote.status, 200);
const res = await api('/notes', { const res = await api('notes', {
withFiles: true, withFiles: true,
}, alice); }, alice);
@ -364,23 +364,23 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id); const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id);
assert.notEqual(myNote, null); assert.notEqual(myNote, null);
assert.strictEqual(myNote.files.length, 1); assert.strictEqual(myNote.files.length, 1);
assert.strictEqual(myNote.files[0].id, file.body.id); assert.strictEqual(myNote.files[0].id, file.body!.id);
}); });
test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const createdNote = await api('/notes/create', { const createdNote = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(createdNote.status, 200); assert.strictEqual(createdNote.status, 200);
const renoted = await api('/notes/create', { const renoted = await api('notes/create', {
renoteId: createdNote.body.createdNote.id, renoteId: createdNote.body.createdNote.id,
}, alice); }, alice);
assert.strictEqual(renoted.status, 200); assert.strictEqual(renoted.status, 200);
const res = await api('/notes', { const res = await api('notes', {
renote: true, renote: true,
}, alice); }, alice);
@ -389,24 +389,24 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null); assert.notEqual(myNote, null);
assert.strictEqual(myNote.renote.files.length, 1); assert.strictEqual(myNote.renote.files.length, 1);
assert.strictEqual(myNote.renote.files[0].id, file.body.id); assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
}); });
test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const createdNote = await api('/notes/create', { const createdNote = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(createdNote.status, 200); assert.strictEqual(createdNote.status, 200);
const reply = await api('/notes/create', { const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id, replyId: createdNote.body.createdNote.id,
text: 'this is reply', text: 'this is reply',
}, alice); }, alice);
assert.strictEqual(reply.status, 200); assert.strictEqual(reply.status, 200);
const res = await api('/notes', { const res = await api('notes', {
reply: true, reply: true,
}, alice); }, alice);
@ -415,29 +415,29 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id); const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
assert.notEqual(myNote, null); assert.notEqual(myNote, null);
assert.strictEqual(myNote.reply.files.length, 1); assert.strictEqual(myNote.reply.files.length, 1);
assert.strictEqual(myNote.reply.files[0].id, file.body.id); assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
}); });
test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const createdNote = await api('/notes/create', { const createdNote = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(createdNote.status, 200); assert.strictEqual(createdNote.status, 200);
const reply = await api('/notes/create', { const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id, replyId: createdNote.body.createdNote.id,
text: 'this is reply', text: 'this is reply',
}, alice); }, alice);
assert.strictEqual(reply.status, 200); assert.strictEqual(reply.status, 200);
const renoted = await api('/notes/create', { const renoted = await api('notes/create', {
renoteId: reply.body.createdNote.id, renoteId: reply.body.createdNote.id,
}, alice); }, alice);
assert.strictEqual(renoted.status, 200); assert.strictEqual(renoted.status, 200);
const res = await api('/notes', { const res = await api('notes', {
renote: true, renote: true,
}, alice); }, alice);
@ -446,7 +446,7 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null); assert.notEqual(myNote, null);
assert.strictEqual(myNote.renote.reply.files.length, 1); assert.strictEqual(myNote.renote.reply.files.length, 1);
assert.strictEqual(myNote.renote.reply.files[0].id, file.body.id); assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
}); });
test('NSFWが強制されている場合変更できない', async () => { test('NSFWが強制されている場合変更できない', async () => {
@ -472,7 +472,7 @@ describe('Note', () => {
priority: 0, priority: 0,
value: true, value: true,
}, },
}, } as any,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -483,15 +483,15 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(assign.status, 204); assert.strictEqual(assign.status, 204);
assert.strictEqual(file.body.isSensitive, false); assert.strictEqual(file.body!.isSensitive, false);
const nsfwfile = await uploadFile(alice); const nsfwfile = await uploadFile(alice);
assert.strictEqual(nsfwfile.status, 200); assert.strictEqual(nsfwfile.status, 200);
assert.strictEqual(nsfwfile.body.isSensitive, true); assert.strictEqual(nsfwfile.body!.isSensitive, true);
const liftnsfw = await api('drive/files/update', { const liftnsfw = await api('drive/files/update', {
fileId: nsfwfile.body.id, fileId: nsfwfile.body!.id,
isSensitive: false, isSensitive: false,
}, alice); }, alice);
@ -499,7 +499,7 @@ describe('Note', () => {
assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE'); assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE');
const oldaddnsfw = await api('drive/files/update', { const oldaddnsfw = await api('drive/files/update', {
fileId: file.body.id, fileId: file.body!.id,
isSensitive: true, isSensitive: true,
}, alice); }, alice);
@ -518,7 +518,7 @@ describe('Note', () => {
describe('notes/create', () => { describe('notes/create', () => {
test('投票を添付できる', async () => { test('投票を添付できる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['foo', 'bar'], choices: ['foo', 'bar'],
@ -531,14 +531,15 @@ describe('Note', () => {
}); });
test('投票の選択肢が無くて怒られる', async () => { test('投票の選択肢が無くて怒られる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
// @ts-expect-error poll must not be empty
poll: {}, poll: {},
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('投票の選択肢が無くて怒られる (空の配列)', async () => { test('投票の選択肢が無くて怒られる (空の配列)', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
poll: { poll: {
choices: [], choices: [],
}, },
@ -547,7 +548,7 @@ describe('Note', () => {
}); });
test('投票の選択肢が1つで怒られる', async () => { test('投票の選択肢が1つで怒られる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
poll: { poll: {
choices: ['Strawberry Pasta'], choices: ['Strawberry Pasta'],
}, },
@ -556,14 +557,14 @@ describe('Note', () => {
}); });
test('投票できる', async () => { test('投票できる', async () => {
const { body } = await api('/notes/create', { const { body } = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['sakura', 'izumi', 'ako'], choices: ['sakura', 'izumi', 'ako'],
}, },
}, alice); }, alice);
const res = await api('/notes/polls/vote', { const res = await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 1, choice: 1,
}, alice); }, alice);
@ -572,19 +573,19 @@ describe('Note', () => {
}); });
test('複数投票できない', async () => { test('複数投票できない', async () => {
const { body } = await api('/notes/create', { const { body } = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['sakura', 'izumi', 'ako'], choices: ['sakura', 'izumi', 'ako'],
}, },
}, alice); }, alice);
await api('/notes/polls/vote', { await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 0, choice: 0,
}, alice); }, alice);
const res = await api('/notes/polls/vote', { const res = await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 2, choice: 2,
}, alice); }, alice);
@ -593,7 +594,7 @@ describe('Note', () => {
}); });
test('許可されている場合は複数投票できる', async () => { test('許可されている場合は複数投票できる', async () => {
const { body } = await api('/notes/create', { const { body } = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['sakura', 'izumi', 'ako'], choices: ['sakura', 'izumi', 'ako'],
@ -601,17 +602,17 @@ describe('Note', () => {
}, },
}, alice); }, alice);
await api('/notes/polls/vote', { await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 0, choice: 0,
}, alice); }, alice);
await api('/notes/polls/vote', { await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 1, choice: 1,
}, alice); }, alice);
const res = await api('/notes/polls/vote', { const res = await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 2, choice: 2,
}, alice); }, alice);
@ -620,7 +621,7 @@ describe('Note', () => {
}); });
test('締め切られている場合は投票できない', async () => { test('締め切られている場合は投票できない', async () => {
const { body } = await api('/notes/create', { const { body } = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['sakura', 'izumi', 'ako'], choices: ['sakura', 'izumi', 'ako'],
@ -630,7 +631,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const res = await api('/notes/polls/vote', { const res = await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 1, choice: 1,
}, alice); }, alice);
@ -649,7 +650,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', { const note1 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, alice); }, alice);
@ -666,7 +667,7 @@ describe('Note', () => {
assert.strictEqual(sensitive.status, 204); assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', { const note2 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, alice); }, alice);
@ -683,7 +684,7 @@ describe('Note', () => {
assert.strictEqual(sensitive.status, 204); assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', { const note2 = await api('notes/create', {
text: 'hogeTesthuge', text: 'hogeTesthuge',
}, alice); }, alice);
@ -702,7 +703,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', { const note1 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, alice); }, alice);
@ -719,7 +720,7 @@ describe('Note', () => {
assert.strictEqual(prohibited.status, 204); assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', { const note2 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, alice); }, alice);
@ -736,7 +737,7 @@ describe('Note', () => {
assert.strictEqual(prohibited.status, 204); assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', { const note2 = await api('notes/create', {
text: 'hogeTesthuge', text: 'hogeTesthuge',
}, alice); }, alice);
@ -755,7 +756,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', { const note1 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, tom); }, tom);
@ -783,7 +784,7 @@ describe('Note', () => {
priority: 1, priority: 1,
value: 0, value: 0,
}, },
}, } as any,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -799,7 +800,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', { const note = await api('notes/create', {
text: '@bob potentially annoying text', text: '@bob potentially annoying text',
}, alice); }, alice);
@ -837,7 +838,7 @@ describe('Note', () => {
priority: 1, priority: 1,
value: 0, value: 0,
}, },
}, } as any,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -853,10 +854,10 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', { const note = await api('notes/create', {
text: 'potentially annoying text', text: 'potentially annoying text',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [ bob.id ], visibleUserIds: [bob.id],
}, alice); }, alice);
assert.strictEqual(note.status, 400); assert.strictEqual(note.status, 400);
@ -893,7 +894,7 @@ describe('Note', () => {
priority: 1, priority: 1,
value: 1, value: 1,
}, },
}, } as any,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -909,10 +910,10 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', { const note = await api('notes/create', {
text: '@bob potentially annoying text', text: '@bob potentially annoying text',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [ bob.id ], visibleUserIds: [bob.id],
}, alice); }, alice);
assert.strictEqual(note.status, 200); assert.strictEqual(note.status, 200);

View file

@ -22,7 +22,7 @@ describe('Renote Mute', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('ミュート作成', async () => { test('ミュート作成', async () => {
const res = await api('/renote-mute/create', { const res = await api('renote-mute/create', {
userId: carol.id, userId: carol.id,
}, alice); }, alice);
@ -37,7 +37,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await sleep(100);
const res = await api('/notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -54,7 +54,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await sleep(100);
const res = await api('/notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);

View file

@ -601,7 +601,7 @@ describe('Streaming', () => {
// #10443 // #10443
test('ミュートしているサーバのートがリストTLに流れない', async () => { test('ミュートしているサーバのートがリストTLに流れない', async () => {
await api('/i/update', { await api('i/update', {
mutedInstances: ['example.com'], mutedInstances: ['example.com'],
}, chitose); }, chitose);
@ -618,7 +618,7 @@ describe('Streaming', () => {
// #10443 // #10443
test('ミュートしているサーバのートに対するリプライがリストTLに流れない', async () => { test('ミュートしているサーバのートに対するリプライがリストTLに流れない', async () => {
await api('/i/update', { await api('i/update', {
mutedInstances: ['example.com'], mutedInstances: ['example.com'],
}, chitose); }, chitose);
@ -635,7 +635,7 @@ describe('Streaming', () => {
// #10443 // #10443
test('ミュートしているサーバのートに対するリートがリストTLに流れない', async () => { test('ミュートしているサーバのートに対するリートがリストTLに流れない', async () => {
await api('/i/update', { await api('i/update', {
mutedInstances: ['example.com'], mutedInstances: ['example.com'],
}, chitose); }, chitose);

View file

@ -24,12 +24,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await api('/notes/mentions', {}, alice); const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -40,15 +40,15 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const res = await api('/i', {}, alice); const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false); assert.strictEqual(res.body.hasUnreadMentions, false);
@ -56,11 +56,11 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise<void>(async done => { test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise<void>(async done => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
let fired = false; let fired = false;
@ -84,12 +84,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);

File diff suppressed because it is too large Load diff

View file

@ -11,9 +11,9 @@ import type * as misskey from 'misskey-js';
describe('users/notes', () => { describe('users/notes', () => {
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let jpgNote: any; let jpgNote: misskey.entities.Note;
let pngNote: any; let pngNote: misskey.entities.Note;
let jpgPngNote: any; let jpgPngNote: misskey.entities.Note;
beforeAll(async () => { beforeAll(async () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
@ -31,7 +31,7 @@ describe('users/notes', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('withFiles', async () => { test('withFiles', async () => {
const res = await api('/users/notes', { const res = await api('users/notes', {
userId: alice.id, userId: alice.id,
withFiles: true, withFiles: true,
}, alice); }, alice);

View file

@ -8,7 +8,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { inspect } from 'node:util'; import { inspect } from 'node:util';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('ユーザー', () => { describe('ユーザー', () => {
@ -24,31 +24,12 @@ describe('ユーザー', () => {
}, {}); }, {});
}; };
// BUG misskey-jsとjson-schemaと実際に返ってくるデータが全部違う const show = async (id: string, me = root): Promise<misskey.entities.UserDetailed> => {
type UserLite = misskey.entities.UserLite & { return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me });
badgeRoles: any[],
};
type UserDetailedNotMe = UserLite &
misskey.entities.UserDetailed & {
roles: any[],
};
type MeDetailed = UserDetailedNotMe &
misskey.entities.MeDetailed & {
achievements: object[],
loggedInDays: number,
policies: object,
};
type User = MeDetailed & { token: string };
const show = async (id: string, me = root): Promise<MeDetailed | UserDetailedNotMe> => {
return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any;
}; };
// UserLiteのキーが過不足なく入っている // UserLiteのキーが過不足なく入っている
const userLite = (user: User): Partial<UserLite> => { const userLite = (user: misskey.entities.UserLite): Partial<misskey.entities.UserLite> => {
return stripUndefined({ return stripUndefined({
id: user.id, id: user.id,
name: user.name, name: user.name,
@ -72,7 +53,7 @@ describe('ユーザー', () => {
}; };
// UserDetailedNotMeのキーが過不足なく入っている // UserDetailedNotMeのキーが過不足なく入っている
const userDetailedNotMe = (user: User): Partial<UserDetailedNotMe> => { const userDetailedNotMe = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({ return stripUndefined({
...userLite(user), ...userLite(user),
url: user.url, url: user.url,
@ -114,7 +95,7 @@ describe('ユーザー', () => {
}; };
// Relations関連のキーが過不足なく入っている // Relations関連のキーが過不足なく入っている
const userDetailedNotMeWithRelations = (user: User): Partial<UserDetailedNotMe> => { const userDetailedNotMeWithRelations = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({ return stripUndefined({
...userDetailedNotMe(user), ...userDetailedNotMe(user),
isFollowing: user.isFollowing ?? false, isFollowing: user.isFollowing ?? false,
@ -131,7 +112,7 @@ describe('ユーザー', () => {
}; };
// MeDetailedのキーが過不足なく入っている // MeDetailedのキーが過不足なく入っている
const meDetailed = (user: User, security = false): Partial<MeDetailed> => { const meDetailed = (user: misskey.entities.SignupResponse, security = false): Partial<misskey.entities.MeDetailed> => {
return stripUndefined({ return stripUndefined({
...userDetailedNotMe(user), ...userDetailedNotMe(user),
avatarId: user.avatarId, avatarId: user.avatarId,
@ -162,6 +143,7 @@ describe('ユーザー', () => {
mutedWords: user.mutedWords, mutedWords: user.mutedWords,
hardMutedWords: user.hardMutedWords, hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances, mutedInstances: user.mutedInstances,
// @ts-expect-error 後方互換性
mutingNotificationTypes: user.mutingNotificationTypes, mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig, notificationRecieveConfig: user.notificationRecieveConfig,
emailNotificationTypes: user.emailNotificationTypes, emailNotificationTypes: user.emailNotificationTypes,
@ -176,61 +158,53 @@ describe('ユーザー', () => {
}); });
}; };
let root: User; let root: misskey.entities.SignupResponse;
let alice: User; let alice: misskey.entities.SignupResponse;
let aliceNote: misskey.entities.Note; let aliceNote: misskey.entities.Note;
let alicePage: misskey.entities.Page;
let aliceList: misskey.entities.UserList;
let bob: User; let bob: misskey.entities.SignupResponse;
let bobNote: misskey.entities.Note;
let carol: User; // NOTE: これがないと落ちるbob の updatedAt が null になってしまうため?)
let dave: User; let bobNote: misskey.entities.Note; // eslint-disable-line @typescript-eslint/no-unused-vars
let ellen: User;
let frank: User;
let usersReplying: User[]; let carol: misskey.entities.SignupResponse;
let userNoNote: User; let usersReplying: misskey.entities.SignupResponse[];
let userNotExplorable: User;
let userLocking: User; let userNoNote: misskey.entities.SignupResponse;
let userAdmin: User; let userNotExplorable: misskey.entities.SignupResponse;
let roleAdmin: any; let userLocking: misskey.entities.SignupResponse;
let userModerator: User; let userAdmin: misskey.entities.SignupResponse;
let roleModerator: any; let roleAdmin: misskey.entities.Role;
let userRolePublic: User; let userModerator: misskey.entities.SignupResponse;
let rolePublic: any; let roleModerator: misskey.entities.Role;
let userRoleBadge: User; let userRolePublic: misskey.entities.SignupResponse;
let roleBadge: any; let rolePublic: misskey.entities.Role;
let userSilenced: User; let userRoleBadge: misskey.entities.SignupResponse;
let roleSilenced: any; let roleBadge: misskey.entities.Role;
let userSuspended: User; let userSilenced: misskey.entities.SignupResponse;
let userDeletedBySelf: User; let roleSilenced: misskey.entities.Role;
let userDeletedByAdmin: User; let userSuspended: misskey.entities.SignupResponse;
let userFollowingAlice: User; let userDeletedBySelf: misskey.entities.SignupResponse;
let userFollowedByAlice: User; let userDeletedByAdmin: misskey.entities.SignupResponse;
let userBlockingAlice: User; let userFollowingAlice: misskey.entities.SignupResponse;
let userBlockedByAlice: User; let userFollowedByAlice: misskey.entities.SignupResponse;
let userMutingAlice: User; let userBlockingAlice: misskey.entities.SignupResponse;
let userMutedByAlice: User; let userBlockedByAlice: misskey.entities.SignupResponse;
let userRnMutingAlice: User; let userMutingAlice: misskey.entities.SignupResponse;
let userRnMutedByAlice: User; let userMutedByAlice: misskey.entities.SignupResponse;
let userFollowRequesting: User; let userRnMutingAlice: misskey.entities.SignupResponse;
let userFollowRequested: User; let userRnMutedByAlice: misskey.entities.SignupResponse;
let userFollowRequesting: misskey.entities.SignupResponse;
let userFollowRequested: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
root = await signup({ username: 'root' }); root = await signup({ username: 'root' });
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
aliceNote = await post(alice, { text: 'test' }) as any; aliceNote = await post(alice, { text: 'test' });
alicePage = await page(alice);
aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body;
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
bobNote = await post(bob, { text: 'test' }) as any; bobNote = await post(bob, { text: 'test' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
dave = await signup({ username: 'dave' });
ellen = await signup({ username: 'ellen' });
frank = await signup({ username: 'frank' });
// @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする // @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする
usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => { usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => {
@ -241,7 +215,7 @@ describe('ユーザー', () => {
} }
return (await acc).concat(u); return (await acc).concat(u);
}, Promise.resolve([] as User[])); }, Promise.resolve([] as misskey.entities.SignupResponse[]));
userNoNote = await signup({ username: 'userNoNote' }); userNoNote = await signup({ username: 'userNoNote' });
userNotExplorable = await signup({ username: 'userNotExplorable' }); userNotExplorable = await signup({ username: 'userNotExplorable' });
@ -309,7 +283,7 @@ describe('ユーザー', () => {
beforeEach(async () => { beforeEach(async () => {
alice = { alice = {
...alice, ...alice,
...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }) as any, ...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }),
}; };
aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice }); aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice });
}); });
@ -322,7 +296,7 @@ describe('ユーザー', () => {
endpoint: 'signup', endpoint: 'signup',
parameters: { username: 'zoe', password: 'password' }, parameters: { username: 'zoe', password: 'password' },
user: undefined, user: undefined,
}) as unknown as User; // BUG MeDetailedに足りないキーがある }) as unknown as misskey.entities.SignupResponse; // BUG MeDetailedに足りないキーがある
// signupの時はtokenが含まれる特別なMeDetailedが返ってくる // signupの時はtokenが含まれる特別なMeDetailedが返ってくる
assert.match(response.token, /[a-zA-Z0-9]{16}/); assert.match(response.token, /[a-zA-Z0-9]{16}/);
@ -332,7 +306,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.name, null); assert.strictEqual(response.name, null);
assert.strictEqual(response.username, 'zoe'); assert.strictEqual(response.username, 'zoe');
assert.strictEqual(response.host, null); assert.strictEqual(response.host, null);
assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); response.avatarUrl && assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.strictEqual(response.avatarBlurhash, null); assert.strictEqual(response.avatarBlurhash, null);
assert.deepStrictEqual(response.avatarDecorations, []); assert.deepStrictEqual(response.avatarDecorations, []);
assert.strictEqual(response.isBot, false); assert.strictEqual(response.isBot, false);
@ -407,6 +381,7 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.unreadAnnouncements, []); assert.deepStrictEqual(response.unreadAnnouncements, []);
assert.deepStrictEqual(response.mutedWords, []); assert.deepStrictEqual(response.mutedWords, []);
assert.deepStrictEqual(response.mutedInstances, []); assert.deepStrictEqual(response.mutedInstances, []);
// @ts-expect-error 後方互換のため
assert.deepStrictEqual(response.mutingNotificationTypes, []); assert.deepStrictEqual(response.mutingNotificationTypes, []);
assert.deepStrictEqual(response.notificationRecieveConfig, {}); assert.deepStrictEqual(response.notificationRecieveConfig, {});
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']); assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
@ -436,68 +411,66 @@ describe('ユーザー', () => {
//#region 自分の情報の更新(i/update) //#region 自分の情報の更新(i/update)
test.each([ test.each([
{ parameters: (): object => ({ name: null }) }, { parameters: () => ({ name: null }) },
{ parameters: (): object => ({ name: 'x'.repeat(50) }) }, { parameters: () => ({ name: 'x'.repeat(50) }) },
{ parameters: (): object => ({ name: 'x' }) }, { parameters: () => ({ name: 'x' }) },
{ parameters: (): object => ({ name: 'My name' }) }, { parameters: () => ({ name: 'My name' }) },
{ parameters: (): object => ({ description: null }) }, { parameters: () => ({ description: null }) },
{ parameters: (): object => ({ description: 'x'.repeat(1500) }) }, { parameters: () => ({ description: 'x'.repeat(1500) }) },
{ parameters: (): object => ({ description: 'x' }) }, { parameters: () => ({ description: 'x' }) },
{ parameters: (): object => ({ description: 'My description' }) }, { parameters: () => ({ description: 'My description' }) },
{ parameters: (): object => ({ location: null }) }, { parameters: () => ({ location: null }) },
{ parameters: (): object => ({ location: 'x'.repeat(50) }) }, { parameters: () => ({ location: 'x'.repeat(50) }) },
{ parameters: (): object => ({ location: 'x' }) }, { parameters: () => ({ location: 'x' }) },
{ parameters: (): object => ({ location: 'My location' }) }, { parameters: () => ({ location: 'My location' }) },
{ parameters: (): object => ({ birthday: '0000-00-00' }) }, { parameters: () => ({ birthday: '0000-00-00' }) },
{ parameters: (): object => ({ birthday: '9999-99-99' }) }, { parameters: () => ({ birthday: '9999-99-99' }) },
{ parameters: (): object => ({ lang: 'en-US' }) }, { parameters: () => ({ lang: 'en-US' as const }) },
{ parameters: (): object => ({ fields: [] }) }, { parameters: () => ({ fields: [] }) },
{ parameters: (): object => ({ fields: [{ name: 'x', value: 'x' }] }) }, { parameters: () => ({ fields: [{ name: 'x', value: 'x' }] }) },
{ parameters: (): object => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない { parameters: () => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない
{ parameters: (): object => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) }, { parameters: () => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) },
{ parameters: (): object => ({ isLocked: true }) }, { parameters: () => ({ isLocked: true }) },
{ parameters: (): object => ({ isLocked: false }) }, { parameters: () => ({ isLocked: false }) },
{ parameters: (): object => ({ isExplorable: false }) }, { parameters: () => ({ isExplorable: false }) },
{ parameters: (): object => ({ isExplorable: true }) }, { parameters: () => ({ isExplorable: true }) },
{ parameters: (): object => ({ hideOnlineStatus: true }) }, { parameters: () => ({ hideOnlineStatus: true }) },
{ parameters: (): object => ({ hideOnlineStatus: false }) }, { parameters: () => ({ hideOnlineStatus: false }) },
{ parameters: (): object => ({ publicReactions: false }) }, { parameters: () => ({ publicReactions: false }) },
{ parameters: (): object => ({ publicReactions: true }) }, { parameters: () => ({ publicReactions: true }) },
{ parameters: (): object => ({ autoAcceptFollowed: true }) }, { parameters: () => ({ autoAcceptFollowed: true }) },
{ parameters: (): object => ({ autoAcceptFollowed: false }) }, { parameters: () => ({ autoAcceptFollowed: false }) },
{ parameters: (): object => ({ noCrawle: true }) }, { parameters: () => ({ noCrawle: true }) },
{ parameters: (): object => ({ noCrawle: false }) }, { parameters: () => ({ noCrawle: false }) },
{ parameters: (): object => ({ preventAiLearning: false }) }, { parameters: () => ({ preventAiLearning: false }) },
{ parameters: (): object => ({ preventAiLearning: true }) }, { parameters: () => ({ preventAiLearning: true }) },
{ parameters: (): object => ({ isBot: true }) }, { parameters: () => ({ isBot: true }) },
{ parameters: (): object => ({ isBot: false }) }, { parameters: () => ({ isBot: false }) },
{ parameters: (): object => ({ isCat: true }) }, { parameters: () => ({ isCat: true }) },
{ parameters: (): object => ({ isCat: false }) }, { parameters: () => ({ isCat: false }) },
{ parameters: (): object => ({ speakAsCat: true }) }, { parameters: () => ({ injectFeaturedNote: true }) },
{ parameters: (): object => ({ speakAsCat: false }) }, { parameters: () => ({ injectFeaturedNote: false }) },
{ parameters: (): object => ({ injectFeaturedNote: true }) }, { parameters: () => ({ receiveAnnouncementEmail: true }) },
{ parameters: (): object => ({ injectFeaturedNote: false }) }, { parameters: () => ({ receiveAnnouncementEmail: false }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) }, { parameters: () => ({ alwaysMarkNsfw: true }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: false }) }, { parameters: () => ({ alwaysMarkNsfw: false }) },
{ parameters: (): object => ({ alwaysMarkNsfw: true }) }, { parameters: () => ({ autoSensitive: true }) },
{ parameters: (): object => ({ alwaysMarkNsfw: false }) }, { parameters: () => ({ autoSensitive: false }) },
{ parameters: (): object => ({ autoSensitive: true }) }, { parameters: () => ({ followingVisibility: 'private' as const }) },
{ parameters: (): object => ({ autoSensitive: false }) }, { parameters: () => ({ followingVisibility: 'followers' as const }) },
{ parameters: (): object => ({ followingVisibility: 'private' }) }, { parameters: () => ({ followingVisibility: 'public' as const }) },
{ parameters: (): object => ({ followingVisibility: 'followers' }) }, { parameters: () => ({ followersVisibility: 'private' as const }) },
{ parameters: (): object => ({ followingVisibility: 'public' }) }, { parameters: () => ({ followersVisibility: 'followers' as const }) },
{ parameters: (): object => ({ followersVisibility: 'private' }) }, { parameters: () => ({ followersVisibility: 'public' as const }) },
{ parameters: (): object => ({ followersVisibility: 'followers' }) }, { parameters: () => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
{ parameters: (): object => ({ followersVisibility: 'public' }) }, { parameters: () => ({ mutedWords: [['x'.repeat(194)]] }) },
{ parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, { parameters: () => ({ mutedWords: [] }) },
{ parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, { parameters: () => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: (): object => ({ mutedWords: [] }) }, { parameters: () => ({ mutedInstances: [] }) },
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) }, { parameters: () => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
{ parameters: (): object => ({ mutedInstances: [] }) }, { parameters: () => ({ notificationRecieveConfig: {} }) },
{ parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) }, { parameters: () => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ notificationRecieveConfig: {} }) }, { parameters: () => ({ emailNotificationTypes: [] }) },
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
] as const)('を書き換えることができる($#)', async ({ parameters }) => { ] as const)('を書き換えることができる($#)', async ({ parameters }) => {
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice }); const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice });
const expected = { ...meDetailed(alice, true), ...parameters() }; const expected = { ...meDetailed(alice, true), ...parameters() };
@ -506,13 +479,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Avatar)', async () => { test('を書き換えることができる(Avatar)', async () => {
const aliceFile = (await uploadFile(alice)).body; const aliceFile = (await uploadFile(alice)).body;
const parameters = { avatarId: aliceFile.id }; const parameters = { avatarId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/); assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/);
const expected = { const expected = {
...meDetailed(alice, true), ...meDetailed(alice, true),
avatarId: aliceFile.id, avatarId: aliceFile!.id,
avatarBlurhash: response.avatarBlurhash, avatarBlurhash: response.avatarBlurhash,
avatarUrl: response.avatarUrl, avatarUrl: response.avatarUrl,
}; };
@ -531,13 +504,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Banner)', async () => { test('を書き換えることができる(Banner)', async () => {
const aliceFile = (await uploadFile(alice)).body; const aliceFile = (await uploadFile(alice)).body;
const parameters = { bannerId: aliceFile.id }; const parameters = { bannerId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/); assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/);
const expected = { const expected = {
...meDetailed(alice, true), ...meDetailed(alice, true),
bannerId: aliceFile.id, bannerId: aliceFile!.id,
bannerBlurhash: response.bannerBlurhash, bannerBlurhash: response.bannerBlurhash,
bannerUrl: response.bannerUrl, bannerUrl: response.bannerUrl,
}; };
@ -612,13 +585,13 @@ describe('ユーザー', () => {
//#region ユーザー(users) //#region ユーザー(users)
test.each([ test.each([
{ label: 'ID昇順', parameters: { limit: 5 }, selector: (u: UserLite): string => u.id }, { label: 'ID昇順', parameters: { limit: 5 }, selector: (u: misskey.entities.UserLite): string => u.id },
{ label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, { label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, { label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, { label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, { label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, { label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, { label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をリスト形式で取得することができる($label', async ({ parameters, selector }) => { ] as const)('をリスト形式で取得することができる($label', async ({ parameters, selector }) => {
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@ -631,15 +604,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれない', user: (): User => userNotExplorable, excluded: true }, { label: '「見つけやすくする」がOFFのユーザーが含まれない', user: () => userNotExplorable, excluded: true },
{ label: 'ミュートユーザーが含まれない', user: (): User => userMutedByAlice, excluded: true }, { label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true },
{ label: 'ブロックされているユーザーが含まれない', user: (): User => userBlockedByAlice, excluded: true }, { label: 'ブロックされているユーザーが含まれない', user: () => userBlockedByAlice, excluded: true },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => { ] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => {
const parameters = { limit: 100 }; const parameters = { limit: 100 };
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@ -653,39 +626,44 @@ describe('ユーザー', () => {
//#region ユーザー情報(users/show) //#region ユーザー情報(users/show)
test.each([ test.each([
{ label: 'ID指定で自分自身を', parameters: (): object => ({ userId: alice.id }), user: (): User => alice, type: meDetailed }, { label: 'ID指定で自分自身を', parameters: () => ({ userId: alice.id }), user: () => alice, type: meDetailed },
{ label: 'ID指定で他人を', parameters: (): object => ({ userId: alice.id }), user: (): User => bob, type: userDetailedNotMeWithRelations }, { label: 'ID指定で他人を', parameters: () => ({ userId: alice.id }), user: () => bob, type: userDetailedNotMeWithRelations },
{ label: 'ID指定かつ未認証', parameters: (): object => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe }, { label: 'ID指定かつ未認証', parameters: () => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe },
{ label: '@指定で自分自身を', parameters: (): object => ({ username: alice.username }), user: (): User => alice, type: meDetailed }, { label: '@指定で自分自身を', parameters: () => ({ username: alice.username }), user: () => alice, type: meDetailed },
{ label: '@指定で他人を', parameters: (): object => ({ username: alice.username }), user: (): User => bob, type: userDetailedNotMeWithRelations }, { label: '@指定で他人を', parameters: () => ({ username: alice.username }), user: () => bob, type: userDetailedNotMeWithRelations },
{ label: '@指定かつ未認証', parameters: (): object => ({ username: alice.username }), user: undefined, type: userDetailedNotMe }, { label: '@指定かつ未認証', parameters: () => ({ username: alice.username }), user: undefined, type: userDetailedNotMe },
] as const)('を取得することができる($label', async ({ parameters, user, type }) => { ] as const)('を取得することができる($label', async ({ parameters, user, type }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() }); const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() });
const expected = type(alice); const expected = type(alice);
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: 'Administratorになっている', user: (): User => userAdmin, me: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin }, { label: 'Administratorになっている', user: () => userAdmin, me: () => userAdmin, selector: (user: misskey.entities.MeDetailed) => user.isAdmin },
{ label: '自分以外から見たときはAdministratorか判定できない', user: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin, expected: (): undefined => undefined }, // @ts-expect-error UserDetailedNotMe doesn't include isAdmin
{ label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator }, { label: '自分以外から見たときはAdministratorか判定できない', user: () => userAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isAdmin, expected: () => undefined },
{ label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined }, { label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
{ label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced }, // @ts-expect-error UserDetailedNotMe doesn't include isModerator
//{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended }, { label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
{ label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted }, { label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
{ label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, // FIXME: 落ちる
{ label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted }, //{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },
{ label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, { label: '削除済みになっている', user: () => userDeletedBySelf, me: () => userDeletedBySelf, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
{ label: 'フォロー中になっている', user: (): User => userFollowedByAlice, selector: (user: User): unknown => user.isFollowing }, // @ts-expect-error UserDetailedNotMe doesn't include isDeleted
{ label: 'フォローされている', user: (): User => userFollowingAlice, selector: (user: User): unknown => user.isFollowed }, { label: '自分以外から見たときは削除済みか判定できない', user: () => userDeletedBySelf, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
{ label: 'ブロック中になっている', user: (): User => userBlockedByAlice, selector: (user: User): unknown => user.isBlocking }, { label: '削除済み(byAdmin)になっている', user: () => userDeletedByAdmin, me: () => userDeletedByAdmin, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
{ label: 'ブロックされている', user: (): User => userBlockingAlice, selector: (user: User): unknown => user.isBlocked }, // @ts-expect-error UserDetailedNotMe doesn't include isDeleted
{ label: 'ミュート中になっている', user: (): User => userMutedByAlice, selector: (user: User): unknown => user.isMuted }, { label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: () => userDeletedByAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
{ label: 'リノートミュート中になっている', user: (): User => userRnMutedByAlice, selector: (user: User): unknown => user.isRenoteMuted }, { label: 'フォロー中になっている', user: () => userFollowedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowing },
{ label: 'フォローリクエスト中になっている', user: (): User => userFollowRequested, me: (): User => userFollowRequesting, selector: (user: User): unknown => user.hasPendingFollowRequestFromYou }, { label: 'フォローされている', user: () => userFollowingAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowed },
{ label: 'フォローリクエストされている', user: (): User => userFollowRequesting, me: (): User => userFollowRequested, selector: (user: User): unknown => user.hasPendingFollowRequestToYou }, { label: 'ブロック中になっている', user: () => userBlockedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocking },
{ label: 'ブロックされている', user: () => userBlockingAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocked },
{ label: 'ミュート中になっている', user: () => userMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isMuted },
{ label: 'リノートミュート中になっている', user: () => userRnMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isRenoteMuted },
{ label: 'フォローリクエスト中になっている', user: () => userFollowRequested, me: () => userFollowRequesting, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestFromYou },
{ label: 'フォローリクエストされている', user: () => userFollowRequesting, me: () => userFollowRequested, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestToYou },
] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => { ] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice }); const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice });
assert.strictEqual(selector(response), (expected ?? ((): true => true))()); assert.strictEqual(selector(response as any), (expected ?? ((): true => true))());
}); });
test('を取得することができ、Publicなロールがセットされていること', async () => { test('を取得することができ、Publicなロールがセットされていること', async () => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice }); const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice });
@ -727,17 +705,18 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: (): User => userSuspended, me: (): User => root }, { label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: () => userSuspended, me: () => root },
// BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる // BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる
//{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true }, //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: () => userSuspended, me: () => bob, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
// @ts-expect-error excluded は上でコメントアウトされているので
] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => { ] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => {
const parameters = { userIds: [user().id] }; const parameters = { userIds: [user().id] };
const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice }); const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice });
@ -762,15 +741,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => { ] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => {
const parameters = { query: user().username, limit: 1 }; const parameters = { query: user().username, limit: 1 };
const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
@ -784,30 +763,30 @@ describe('ユーザー', () => {
//#region ID指定検索(users/search-by-username-and-host) //#region ID指定検索(users/search-by-username-and-host)
test.each([ test.each([
{ label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] }, { label: '自分', parameters: { username: 'alice' }, user: () => [alice] },
{ label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] }, { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: () => [alice] },
{ label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] }, { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: () => [userFollowedByAlice] },
{ label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: (): User[] => [] }, { label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: () => [] },
{ label: 'ローカルの他人1', parameters: { username: 'bob' }, user: (): User[] => [bob] }, { label: 'ローカルの他人1', parameters: { username: 'bob' }, user: () => [bob] },
{ label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: (): User[] => [bob] }, { label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: () => [bob] },
{ label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: (): User[] => [bob] }, { label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: () => [bob] },
{ label: 'ローカル', parameters: { host: null, limit: 1 }, user: (): User[] => [userFollowedByAlice] }, { label: 'ローカル', parameters: { host: null, limit: 1 }, user: () => [userFollowedByAlice] },
{ label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] }, { label: 'ローカル', parameters: { host: '.', limit: 1 }, user: () => [userFollowedByAlice] },
])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => { ])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => {
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
const expected = await Promise.all(user().map(u => show(u.id, alice))); const expected = await Promise.all(user().map(u => show(u.id, alice)));
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => { ] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => {
const parameters = { username: user().username }; const parameters = { username: user().username };
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
@ -829,15 +808,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true }, { label: 'ブロックしてきているユーザーが含まれない', user: () => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
//{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, //{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => { ] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => {
const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0]; const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0];
await post(alice, { text: `@${user().username} test`, replyId: replyTo.id }); await post(alice, { text: `@${user().username} test`, replyId: replyTo.id });
@ -851,12 +830,12 @@ describe('ユーザー', () => {
//#region ハッシュタグ(hashtags/users) //#region ハッシュタグ(hashtags/users)
test.each([ test.each([
{ label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, { label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, { label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, { label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, { label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, { label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, { label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => { ] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => {
const hashtag = 'test_hashtag'; const hashtag = 'test_hashtag';
await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice }); await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice });
@ -870,15 +849,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => { ] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => {
const hashtag = `user_test${user().username}`; const hashtag = `user_test${user().username}`;
if (user() !== userSuspended) { if (user() !== userSuspended) {

7
packages/backend/test/global.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FIXME = any;

View file

@ -4,10 +4,10 @@
*/ */
import Ajv from 'ajv'; import Ajv from 'ajv';
import { Schema } from '@/misc/schema'; import { Schema } from '@/misc/json-schema.js';
export const getValidator = (paramDef: Schema) => { export const getValidator = (paramDef: Schema) => {
const ajv = new Ajv({ const ajv = new Ajv.default({
useDefaults: true, useDefaults: true,
}); });
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);

View file

@ -5,7 +5,7 @@
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noUnusedParameters": false, "noUnusedParameters": false,
"noUnusedLocals": true, "noUnusedLocals": false,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"declaration": false, "declaration": false,
"sourceMap": true, "sourceMap": true,
@ -18,6 +18,7 @@
"strict": true, "strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"skipLibCheck": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"resolveJsonModule": true, "resolveJsonModule": true,

View file

@ -51,7 +51,7 @@ describe('AnnouncementService', () => {
function createAnnouncement(data: Partial<MiAnnouncement & { createdAt: Date }> = {}) { function createAnnouncement(data: Partial<MiAnnouncement & { createdAt: Date }> = {}) {
return announcementsRepository.insert({ return announcementsRepository.insert({
id: genAidx(data.createdAt ?? new Date()), id: genAidx(data.createdAt?.getTime() ?? Date.now()),
updatedAt: null, updatedAt: null,
title: 'Title', title: 'Title',
text: 'Text', text: 'Text',

View file

@ -19,8 +19,8 @@ import { DI } from '@/di-symbols.js';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
function mockRedis() { function mockRedis() {
const hash = {}; const hash = {} as any;
const set = jest.fn((key, value) => { const set = jest.fn((key: string, value) => {
const ret = hash[key]; const ret = hash[key];
hash[key] = value; hash[key] = value;
return ret; return ret;
@ -56,12 +56,13 @@ describe('FetchInstanceMetadataService', () => {
} else if (token === DI.redis) { } else if (token === DI.redis) {
return mockRedis; return mockRedis;
} }
return null;
}) })
.compile(); .compile();
app.enableShutdownHooks(); app.enableShutdownHooks();
fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService); fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService) as jest.Mocked<FetchInstanceMetadataService>;
federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService) as jest.Mocked<FederatedInstanceService>; federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService) as jest.Mocked<FederatedInstanceService>;
redisClient = app.get<Redis>(DI.redis) as jest.Mocked<Redis>; redisClient = app.get<Redis>(DI.redis) as jest.Mocked<Redis>;
httpRequestService = app.get<HttpRequestService>(HttpRequestService) as jest.Mocked<HttpRequestService>; httpRequestService = app.get<HttpRequestService>(HttpRequestService) as jest.Mocked<HttpRequestService>;
@ -74,11 +75,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and update', async () => { test('Lock and update', async () => {
redisClient.set = mockRedis(); redisClient.set = mockRedis();
const now = Date.now(); const now = Date.now();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } }); federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); }); httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@ -88,11 +90,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and don\'t update', async () => { test('Lock and don\'t update', async () => {
redisClient.set = mockRedis(); redisClient.set = mockRedis();
const now = Date.now(); const now = Date.now();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now } }); federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); }); httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@ -101,15 +104,33 @@ describe('FetchInstanceMetadataService', () => {
test('Do nothing when lock not acquired', async () => { test('Do nothing when lock not acquired', async () => {
redisClient.set = mockRedis(); redisClient.set = mockRedis();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } }); const now = Date.now();
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); }); httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.tryLock('example.com');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' }); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(2); expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(0); expect(unlockSpy).toHaveBeenCalledTimes(0);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
}); });
test('Do when lock not acquired but forced', async () => {
redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
expect(tryLockSpy).toHaveBeenCalledTimes(0);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalled();
});
}); });

View file

@ -90,7 +90,8 @@ describe('RelayService', () => {
expect(queueService.deliver).toHaveBeenCalled(); expect(queueService.deliver).toHaveBeenCalled();
expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo'); expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo');
expect(queueService.deliver.mock.lastCall![1]?.object.type).toBe('Follow'); expect(typeof queueService.deliver.mock.lastCall![1]?.object).toBe('object');
expect((queueService.deliver.mock.lastCall![1]?.object as any).type).toBe('Follow');
expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com'); expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com');
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');

View file

@ -228,11 +228,14 @@ describe('RoleService', () => {
}, },
target: 'conditional', target: 'conditional',
condFormula: { condFormula: {
id: '232a4221-9816-49a6-a967-ae0fac52ec5e',
type: 'and', type: 'and',
values: [{ values: [{
id: '2a37ef43-2d93-4c4d-87f6-f2fdb7d9b530',
type: 'followersMoreThanOrEq', type: 'followersMoreThanOrEq',
value: 10, value: 10,
}, { }, {
id: '1bd67839-b126-4f92-bad0-4e285dab453b',
type: 'createdMoreThan', type: 'createdMoreThan',
sec: 60 * 60 * 24 * 7, sec: 60 * 60 * 24 * 7,
}], }],

View file

@ -9,11 +9,10 @@ import { basename, isAbsolute } from 'node:path';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util'; import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws'; import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit } from 'node-fetch'; import fetch, { File, RequestInit, type Headers } from 'node-fetch';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { Packed } from '@/misc/json-schema.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { entities } from '../src/postgres.js'; import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js'; import { loadConfig } from '../src/config.js';
@ -21,7 +20,7 @@ import type * as misskey from 'misskey-js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
interface UserToken { export interface UserToken {
token: string; token: string;
bearer?: boolean; bearer?: boolean;
} }
@ -35,20 +34,15 @@ export const cookie = (me: UserToken): string => {
return `token=${me.token};`; return `token=${me.token};`;
}; };
export const api = async (endpoint: string, params: any, me?: UserToken) => { export type ApiRequest<E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req'] = misskey.Endpoints[E]['req']> = {
const normalized = endpoint.replace(/^\//, ''); endpoint: E,
return await request(`api/${normalized}`, params, me); parameters: P,
};
export type ApiRequest = {
endpoint: string,
parameters: object,
user: UserToken | undefined, user: UserToken | undefined,
}; };
export const successfulApiCall = async <T, >(request: ApiRequest, assertion: { export const successfulApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status?: number, status?: number,
} = {}): Promise<T> => { } = {}): Promise<misskey.api.SwitchCaseResponseType<E, P>> => {
const { endpoint, parameters, user } = request; const { endpoint, parameters, user } = request;
const res = await api(endpoint, parameters, user); const res = await api(endpoint, parameters, user);
const status = assertion.status ?? (res.body == null ? 204 : 200); const status = assertion.status ?? (res.body == null ? 204 : 200);
@ -56,7 +50,7 @@ export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body; return res.body;
}; };
export const failedApiCall = async <T, >(request: ApiRequest, assertion: { export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status: number, status: number,
code: string, code: string,
id: string id: string
@ -70,7 +64,7 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body; return res.body;
}; };
const request = async (path: string, params: any, me?: UserToken): Promise<{ export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{
status: number, status: number,
headers: Headers, headers: Headers,
body: any body: any
@ -86,7 +80,7 @@ const request = async (path: string, params: any, me?: UserToken): Promise<{
bodyAuth.i = me.token; bodyAuth.i = me.token;
} }
const res = await relativeFetch(path, { const res = await relativeFetch(`api/${path}`, {
method: 'POST', method: 'POST',
headers, headers,
body: JSON.stringify(Object.assign(bodyAuth, params)), body: JSON.stringify(Object.assign(bodyAuth, params)),
@ -141,7 +135,7 @@ export const signup = async (params?: Partial<misskey.Endpoints['signup']['req']
return res.body; return res.body;
}; };
export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => { export const post = async (user: UserToken, params: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
const q = params; const q = params;
const res = await api('notes/create', q, user); const res = await api('notes/create', q, user);
@ -159,8 +153,8 @@ export const createAppToken = async (user: UserToken, permissions: (typeof missk
}; };
// 非公開ートをAPI越しに見たときのート NoteEntityService.ts // 非公開ートをAPI越しに見たときのート NoteEntityService.ts
export const hiddenNote = (note: any): any => { export const hiddenNote = (note: misskey.entities.Note): misskey.entities.Note => {
const temp = { const temp: misskey.entities.Note = {
...note, ...note,
fileIds: [], fileIds: [],
files: [], files: [],
@ -173,21 +167,22 @@ export const hiddenNote = (note: any): any => {
return temp; return temp;
}; };
export const react = async (user: UserToken, note: any, reaction: string): Promise<any> => { export const react = async (user: UserToken, note: misskey.entities.Note, reaction: string): Promise<void> => {
await api('notes/reactions/create', { await api('notes/reactions/create', {
noteId: note.id, noteId: note.id,
reaction: reaction, reaction: reaction,
}, user); }, user);
}; };
export const userList = async (user: UserToken, userList: any = {}): Promise<any> => { export const userList = async (user: UserToken, userList: Partial<misskey.entities.UserList> = {}): Promise<misskey.entities.UserList> => {
const res = await api('users/lists/create', { const res = await api('users/lists/create', {
name: 'test', name: 'test',
...userList,
}, user); }, user);
return res.body; return res.body;
}; };
export const page = async (user: UserToken, page: any = {}): Promise<any> => { export const page = async (user: UserToken, page: Partial<misskey.entities.Page> = {}): Promise<misskey.entities.Page> => {
const res = await api('pages/create', { const res = await api('pages/create', {
alignCenter: false, alignCenter: false,
content: [ content: [
@ -198,7 +193,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
}, },
], ],
eyeCatchingImageId: null, eyeCatchingImageId: null,
font: 'sans-serif', font: 'sans-serif' as any,
hideTitleWhenPinned: false, hideTitleWhenPinned: false,
name: '1678594845072', name: '1678594845072',
script: '', script: '',
@ -210,7 +205,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
return res.body; return res.body;
}; };
export const play = async (user: UserToken, play: any = {}): Promise<any> => { export const play = async (user: UserToken, play: Partial<misskey.entities.Flash> = {}): Promise<misskey.entities.Flash> => {
const res = await api('flash/create', { const res = await api('flash/create', {
permissions: [], permissions: [],
script: 'test', script: 'test',
@ -221,7 +216,7 @@ export const play = async (user: UserToken, play: any = {}): Promise<any> => {
return res.body; return res.body;
}; };
export const clip = async (user: UserToken, clip: any = {}): Promise<any> => { export const clip = async (user: UserToken, clip: Partial<misskey.entities.Clip> = {}): Promise<misskey.entities.Clip> => {
const res = await api('clips/create', { const res = await api('clips/create', {
description: null, description: null,
isPublic: true, isPublic: true,
@ -231,18 +226,18 @@ export const clip = async (user: UserToken, clip: any = {}): Promise<any> => {
return res.body; return res.body;
}; };
export const galleryPost = async (user: UserToken, channel: any = {}): Promise<any> => { export const galleryPost = async (user: UserToken, galleryPost: Partial<misskey.entities.GalleryPost> = {}): Promise<misskey.entities.GalleryPost> => {
const res = await api('gallery/posts/create', { const res = await api('gallery/posts/create', {
description: null, description: null,
fileIds: [], fileIds: [],
isSensitive: false, isSensitive: false,
title: 'test', title: 'test',
...channel, ...galleryPost,
}, user); }, user);
return res.body; return res.body;
}; };
export const channel = async (user: UserToken, channel: any = {}): Promise<any> => { export const channel = async (user: UserToken, channel: Partial<misskey.entities.Channel> = {}): Promise<misskey.entities.Channel> => {
const res = await api('channels/create', { const res = await api('channels/create', {
bannerId: null, bannerId: null,
description: null, description: null,
@ -252,7 +247,7 @@ export const channel = async (user: UserToken, channel: any = {}): Promise<any>
return res.body; return res.body;
}; };
export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise<any> => { export const role = async (user: UserToken, role: Partial<misskey.entities.Role> = {}, policies: any = {}): Promise<misskey.entities.Role> => {
const res = await api('admin/roles/create', { const res = await api('admin/roles/create', {
asBadge: false, asBadge: false,
canEditMembersByModerator: false, canEditMembersByModerator: false,
@ -260,7 +255,7 @@ export const role = async (user: UserToken, role: any = {}, policies: any = {}):
condFormula: { condFormula: {
id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85', id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85',
type: 'isRemote', type: 'isRemote',
}, } as any,
description: '', description: '',
displayOrder: 0, displayOrder: 0,
iconUrl: null, iconUrl: null,
@ -298,7 +293,7 @@ interface UploadOptions {
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{
status: number, status: number,
headers: Headers, headers: Headers,
body: misskey.Endpoints['drive/files/create']['res'] | null body: misskey.entities.DriveFile | null
}> => { }> => {
const absPath = path == null const absPath = path == null
? new URL('resources/Lenna.jpg', import.meta.url) ? new URL('resources/Lenna.jpg', import.meta.url)
@ -335,14 +330,14 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
}; };
}; };
export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'DriveFile'>> => { export const uploadUrl = async (user: UserToken, url: string): Promise<misskey.entities.DriveFile> => {
const marker = Math.random().toString(); const marker = Math.random().toString();
const catcher = makeStreamCatcher( const catcher = makeStreamCatcher(
user, user,
'main', 'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>, (msg) => msg.body.file,
60 * 1000, 60 * 1000,
); );

View file

@ -189,14 +189,26 @@ export async function mainBoot() {
if ($i.followersCount >= 500) claimAchievement('followers500'); if ($i.followersCount >= 500) claimAchievement('followers500');
if ($i.followersCount >= 1000) claimAchievement('followers1000'); if ($i.followersCount >= 1000) claimAchievement('followers1000');
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) { const createdAt = new Date($i.createdAt);
const createdAtThreeYearsLater = new Date($i.createdAt);
createdAtThreeYearsLater.setFullYear(createdAtThreeYearsLater.getFullYear() + 3);
if (now >= createdAtThreeYearsLater) {
claimAchievement('passedSinceAccountCreated3');
claimAchievement('passedSinceAccountCreated2');
claimAchievement('passedSinceAccountCreated1');
} else {
const createdAtTwoYearsLater = new Date($i.createdAt);
createdAtTwoYearsLater.setFullYear(createdAtTwoYearsLater.getFullYear() + 2);
if (now >= createdAtTwoYearsLater) {
claimAchievement('passedSinceAccountCreated2');
claimAchievement('passedSinceAccountCreated1');
} else {
const createdAtOneYearLater = new Date($i.createdAt);
createdAtOneYearLater.setFullYear(createdAtOneYearLater.getFullYear() + 1);
if (now >= createdAtOneYearLater) {
claimAchievement('passedSinceAccountCreated1'); claimAchievement('passedSinceAccountCreated1');
} }
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
claimAchievement('passedSinceAccountCreated2');
} }
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
claimAchievement('passedSinceAccountCreated3');
} }
if (claimedAchievements.length >= 30) { if (claimedAchievements.length >= 30) {
@ -231,7 +243,7 @@ export async function mainBoot() {
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
} }

View file

@ -69,7 +69,7 @@ import * as os from '@/os.js';
import bytes from '@/filters/bytes.js'; import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js'; import { hms } from '@/filters/hms.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
import { iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
const props = defineProps<{ const props = defineProps<{
audio: Misskey.entities.DriveFile; audio: Misskey.entities.DriveFile;
@ -99,8 +99,6 @@ function showMenu(ev: MouseEvent) {
if (iAmModerator) { if (iAmModerator) {
menu.push({ menu.push({
type: 'divider',
}, {
text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: props.audio.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg', icon: props.audio.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
danger: true, danger: true,
@ -108,6 +106,17 @@ function showMenu(ev: MouseEvent) {
}); });
} }
if ($i?.id === props.audio.userId) {
menu.push({
type: 'divider',
}, {
type: 'link' as const,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.audio.id}`,
});
}
menuShowing.value = true; menuShowing.value = true;
os.popupMenu(menu, ev.currentTarget ?? ev.target, { os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right', align: 'right',

View file

@ -60,7 +60,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
image: Misskey.entities.DriveFile; image: Misskey.entities.DriveFile;
@ -115,6 +115,13 @@ function showMenu(ev: MouseEvent) {
action: () => { action: () => {
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
}, },
}] : []), ...($i?.id === props.image.userId ? [{
type: 'divider' as const,
}, {
type: 'link' as const,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.image.id}`,
}] : [])], ev.currentTarget ?? ev.target); }] : [])], ev.currentTarget ?? ev.target);
} }

View file

@ -97,7 +97,7 @@ import * as os from '@/os.js';
import { isFullscreenNotSupported } from '@/scripts/device-kind.js'; import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
import hasAudio from '@/scripts/media-has-audio.js'; import hasAudio from '@/scripts/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
import { iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
const props = defineProps<{ const props = defineProps<{
video: Misskey.entities.DriveFile; video: Misskey.entities.DriveFile;
@ -125,8 +125,6 @@ function showMenu(ev: MouseEvent) {
if (iAmModerator) { if (iAmModerator) {
menu.push({ menu.push({
type: 'divider',
}, {
text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: props.video.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg', icon: props.video.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
danger: true, danger: true,
@ -134,6 +132,17 @@ function showMenu(ev: MouseEvent) {
}); });
} }
if ($i?.id === props.video.userId) {
menu.push({
type: 'divider',
}, {
type: 'link' as const,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.video.id}`,
});
}
menuShowing.value = true; menuShowing.value = true;
os.popupMenu(menu, ev.currentTarget ?? ev.target, { os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right', align: 'right',

View file

@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div> </div>
<MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction"> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
<template #more> <template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template> </template>
@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<footer :class="$style.footer"> <footer :class="$style.footer">
<button :class="$style.footerButton" class="_button" @click.stop @click="reply()"> <button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button> </button>
<button <button
v-if="canRenote" v-if="canRenote"
@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()" @mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
> >
<i class="ph-rocket-launch ph-bold ph-lg"></i> <i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ appearNote.renoteCount }}</p> <p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button> </button>
<button v-else :class="$style.footerButton" class="_button" disabled> <button v-else :class="$style.footerButton" class="_button" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i> <i class="ph-prohibit ph-bold ph-lg"></i>
@ -135,12 +135,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()"> <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()">
<i class="ph-heart ph-bold ph-lg"></i> <i class="ph-heart ph-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()"> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i>
</button> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<i class="ph-paperclip ph-bold ph-lg"></i> <i class="ph-paperclip ph-bold ph-lg"></i>
@ -195,6 +195,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
@ -626,6 +627,14 @@ function undoRenote(note) : void {
} }
} }
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
if (props.mock) { if (props.mock) {
return; return;

View file

@ -113,10 +113,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="appearNote.createdAt" mode="detail" colored/> <MkTime :time="appearNote.createdAt" mode="detail" colored/>
</MkA> </MkA>
</div> </div>
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
<button class="_button" :class="$style.noteFooterButton" @click="reply()"> <button class="_button" :class="$style.noteFooterButton" @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button> </button>
<button <button
v-if="canRenote" v-if="canRenote"
@ -127,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote() : boostVisibility()" @mousedown="renoted ? undoRenote() : boostVisibility()"
> >
<i class="ph-rocket-launch ph-bold ph-lg"></i> <i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p> <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button> </button>
<button v-else class="_button" :class="$style.noteFooterButton" disabled> <button v-else class="_button" :class="$style.noteFooterButton" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i> <i class="ph-prohibit ph-bold ph-lg"></i>
@ -144,12 +144,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()"> <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()">
<i class="ph-heart ph-bold ph-lg"></i> <i class="ph-heart ph-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()"> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i>
</button> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
<i class="ph-paperclip ph-bold ph-lg"></i> <i class="ph-paperclip ph-bold ph-lg"></i>
@ -236,6 +236,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
import number from '@/filters/number.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
@ -594,11 +595,11 @@ function like(): void {
} }
} }
function undoReact(note): void { function undoReact(targetNote: Misskey.entities.Note): void {
const oldReaction = note.myReaction; const oldReaction = targetNote.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
misskeyApi('notes/reactions/delete', { misskeyApi('notes/reactions/delete', {
noteId: note.id, noteId: targetNote.id,
}); });
} }
@ -619,6 +620,14 @@ function undoRenote() : void {
} }
} }
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement): boolean => { const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true; if (el.tagName === 'A') return true;

View file

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.head"> <div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/> <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
@ -60,6 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span> <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span> <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span> <span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
@ -211,6 +213,7 @@ const rejectFollowRequest = () => {
} }
.icon_reactionGroup, .icon_reactionGroup,
.icon_reactionGroupHeart,
.icon_renoteGroup { .icon_renoteGroup {
display: grid; display: grid;
align-items: center; align-items: center;
@ -223,11 +226,15 @@ const rejectFollowRequest = () => {
} }
.icon_reactionGroup { .icon_reactionGroup {
background: #e99a0b; background: var(--eventReaction);
}
.icon_reactionGroupHeart {
background: var(--eventReactionHeart);
} }
.icon_renoteGroup { .icon_renoteGroup {
background: #36d298; background: var(--eventRenote);
} }
.icon_app { .icon_app {
@ -256,49 +263,49 @@ const rejectFollowRequest = () => {
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest { .t_follow, .t_followRequestAccepted, .t_receiveFollowRequest {
padding: 3px; padding: 3px;
background: #36aed2; background: var(--eventFollow);
pointer-events: none; pointer-events: none;
} }
.t_renote { .t_renote {
padding: 3px; padding: 3px;
background: #36d298; background: var(--eventRenote);
pointer-events: none; pointer-events: none;
} }
.t_quote { .t_quote {
padding: 3px; padding: 3px;
background: #36d298; background: var(--eventRenote);
pointer-events: none; pointer-events: none;
} }
.t_reply { .t_reply {
padding: 3px; padding: 3px;
background: #007aff; background: var(--eventReply);
pointer-events: none; pointer-events: none;
} }
.t_mention { .t_mention {
padding: 3px; padding: 3px;
background: #88a6b7; background: var(--eventOther);
pointer-events: none; pointer-events: none;
} }
.t_pollEnded { .t_pollEnded {
padding: 3px; padding: 3px;
background: #88a6b7; background: var(--eventOther);
pointer-events: none; pointer-events: none;
} }
.t_achievementEarned { .t_achievementEarned {
padding: 3px; padding: 3px;
background: #cb9a11; background: var(--eventAchievement);
pointer-events: none; pointer-events: none;
} }
.t_roleAssigned { .t_roleAssigned {
padding: 3px; padding: 3px;
background: #88a6b7; background: var(--eventOther);
pointer-events: none; pointer-events: none;
} }

View file

@ -63,6 +63,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
reactionAcceptance: null, reactionAcceptance: null,
renoteCount: 0, renoteCount: 0,
repliesCount: 1, repliesCount: 1,
reactionCount: 0,
reactions: {}, reactions: {},
reactionEmojis: {}, reactionEmojis: {},
fileIds: [], fileIds: [],

View file

@ -68,6 +68,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
reactionAcceptance: null, reactionAcceptance: null,
renoteCount: 0, renoteCount: 0,
repliesCount: 1, repliesCount: 1,
reactionCount: 0,
reactions: {}, reactions: {},
reactionEmojis: {}, reactionEmojis: {},
fileIds: [], fileIds: [],

View file

@ -58,6 +58,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
reactionAcceptance: null, reactionAcceptance: null,
renoteCount: 0, renoteCount: 0,
repliesCount: 1, repliesCount: 1,
reactionCount: 0,
reactions: {}, reactions: {},
reactionEmojis: {}, reactionEmojis: {},
fileIds: ['0000000002'], fileIds: ['0000000002'],

View file

@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div> </div>
<MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction"> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
<template #more> <template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template> </template>
@ -107,7 +107,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<footer :class="$style.footer"> <footer :class="$style.footer">
<button :class="$style.footerButton" class="_button" @click.stop @click="reply()"> <button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button> </button>
<button <button
v-if="canRenote" v-if="canRenote"
@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()" @mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
> >
<i class="ph-rocket-launch ph-bold ph-lg"></i> <i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ appearNote.renoteCount }}</p> <p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button> </button>
<button v-else :class="$style.footerButton" class="_button" disabled> <button v-else :class="$style.footerButton" class="_button" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i> <i class="ph-prohibit ph-bold ph-lg"></i>
@ -137,12 +137,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()"> <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()">
<i class="ph-heart ph-bold ph-lg"></i> <i class="ph-heart ph-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()"> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i>
</button> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<i class="ph-paperclip ph-bold ph-lg"></i> <i class="ph-paperclip ph-bold ph-lg"></i>
@ -196,6 +196,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
@ -627,6 +628,14 @@ function undoRenote(note) : void {
} }
} }
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
if (props.mock) { if (props.mock) {
return; return;

View file

@ -120,11 +120,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="appearNote.createdAt" mode="detail" colored/> <MkTime :time="appearNote.createdAt" mode="detail" colored/>
</MkA> </MkA>
</div> </div>
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
<footer :class="$style.footer"> <footer :class="$style.footer">
<button class="_button" :class="$style.noteFooterButton" @click="reply()"> <button class="_button" :class="$style.noteFooterButton" @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button> </button>
<button <button
v-if="canRenote" v-if="canRenote"
@ -135,7 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote() : boostVisibility()" @mousedown="renoted ? undoRenote() : boostVisibility()"
> >
<i class="ph-rocket-launch ph-bold ph-lg"></i> <i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p> <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button> </button>
<button v-else class="_button" :class="$style.noteFooterButton" disabled> <button v-else class="_button" :class="$style.noteFooterButton" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i> <i class="ph-prohibit ph-bold ph-lg"></i>
@ -152,12 +152,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()"> <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()">
<i class="ph-heart ph-bold ph-lg"></i> <i class="ph-heart ph-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()"> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i>
</button> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
<i class="ph-paperclip ph-bold ph-lg"></i> <i class="ph-paperclip ph-bold ph-lg"></i>
@ -243,6 +243,7 @@ import SkInstanceTicker from '@/components/SkInstanceTicker.vue';
import { pleaseLogin } from '@/scripts/please-login.js'; import { pleaseLogin } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
@ -603,11 +604,11 @@ function like(): void {
} }
} }
function undoReact(note): void { function undoReact(targetNote: Misskey.entities.Note): void {
const oldReaction = note.myReaction; const oldReaction = targetNote.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
misskeyApi('notes/reactions/delete', { misskeyApi('notes/reactions/delete', {
noteId: note.id, noteId: targetNote.id,
}); });
} }
@ -628,6 +629,14 @@ function undoRenote() : void {
} }
} }
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement): boolean => { const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true; if (el.tagName === 'A') return true;

View file

@ -14,10 +14,20 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.form_vertical]: chosen.place === 'vertical', [$style.form_vertical]: chosen.place === 'vertical',
}]" }]"
> >
<a :href="chosen.url" target="_blank" :class="$style.link"> <component
:is="self ? 'MkA' : 'a'"
:class="$style.link"
v-bind="self ? {
to: chosen.url.substring(local.length),
} : {
href: chosen.url,
rel: 'nofollow noopener',
target: '_blank',
}"
>
<img :src="chosen.imageUrl" :class="$style.img"> <img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ph-info ph-bold ph-lg"></i></button> <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ph-info ph-bold ph-lg"></i></button>
</a> </component>
</div> </div>
<div v-else :class="$style.menu"> <div v-else :class="$style.menu">
<div :class="$style.menuContainer"> <div :class="$style.menuContainer">
@ -32,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { host } from '@/config.js'; import { url as local, host } from '@/config.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -96,6 +106,9 @@ const choseAd = (): Ad | null => {
}; };
const chosen = ref(choseAd()); const chosen = ref(choseAd());
const self = computed(() => chosen.value?.url.startsWith(local));
const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
function reduceFrequency(): void { function reduceFrequency(): void {

View file

@ -373,7 +373,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
this.currentRoute.value = res.route; this.currentRoute.value = res.route;
this.currentKey = res.route.globalCacheKey ?? key ?? path; this.currentKey = res.route.globalCacheKey ?? key ?? path;
if (emitChange) { if (emitChange && res.route.path !== '/:(*)') {
this.emit('change', { this.emit('change', {
beforePath, beforePath,
path, path,
@ -408,6 +408,9 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
if (cancel) return; if (cancel) return;
} }
const res = this.navigate(path, null); const res = this.navigate(path, null);
if (res.route.path === '/:(*)') {
location.href = path;
} else {
this.emit('push', { this.emit('push', {
beforePath, beforePath,
path: res._parsedRoute.fullPath, path: res._parsedRoute.fullPath,
@ -416,6 +419,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
key: this.currentKey, key: this.currentKey,
}); });
} }
}
public replace(path: string, key?: string | null) { public replace(path: string, key?: string | null) {
const res = this.navigate(path, key); const res = this.navigate(path, key);

View file

@ -60,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch> <MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
<MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch>
<MkSwitch v-model="showReactionsCount">{{ i18n.ts.showReactionsCount }}</MkSwitch>
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch> <MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch> <MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
<MkSwitch v-model="showTickerOnReplies">Show instance ticker on replies</MkSwitch> <MkSwitch v-model="showTickerOnReplies">Show instance ticker on replies</MkSwitch>
@ -328,6 +329,7 @@ const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline')); const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm')); const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm')); const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction')); const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));

View file

@ -70,6 +70,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'animation', 'animation',
'animatedMfm', 'animatedMfm',
'advancedMfm', 'advancedMfm',
'showReactionsCount',
'loadRawImages', 'loadRawImages',
'warnMissingAltText', 'warnMissingAltText',
'imageNewTab', 'imageNewTab',

View file

@ -54,6 +54,7 @@ export function useNoteCapture(props: {
const currentCount = (note.value.reactions || {})[reaction] || 0; const currentCount = (note.value.reactions || {})[reaction] || 0;
note.value.reactions[reaction] = currentCount + 1; note.value.reactions[reaction] = currentCount + 1;
note.value.reactionCount += 1;
if ($i && (body.userId === $i.id)) { if ($i && (body.userId === $i.id)) {
note.value.myReaction = reaction; note.value.myReaction = reaction;
@ -68,6 +69,7 @@ export function useNoteCapture(props: {
const currentCount = (note.value.reactions || {})[reaction] || 0; const currentCount = (note.value.reactions || {})[reaction] || 0;
note.value.reactions[reaction] = Math.max(0, currentCount - 1); note.value.reactions[reaction] = Math.max(0, currentCount - 1);
note.value.reactionCount = Math.max(0, note.value.reactionCount - 1);
if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction]; if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction];
if ($i && (body.userId === $i.id)) { if ($i && (body.userId === $i.id)) {

View file

@ -256,6 +256,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: true, default: true,
}, },
showReactionsCount: {
where: 'device',
default: false,
},
enableQuickAddMfmFunction: { enableQuickAddMfmFunction: {
where: 'device', where: 'device',
default: false, default: false,

View file

@ -41,6 +41,13 @@
--thread-width: 2px; --thread-width: 2px;
//--ad: rgb(255 169 0 / 10%); //--ad: rgb(255 169 0 / 10%);
--eventFollow: #36aed2;
--eventRenote: #36d298;
--eventReply: #007aff;
--eventReactionHeart: #dd2e44;
--eventReaction: #e99a0b;
--eventAchievement: #cb9a11;
--eventOther: #88a6b7;
} }
html.radius-misskey { html.radius-misskey {

View file

@ -49,6 +49,9 @@ const devConfig = {
}, },
'/url': httpUrl, '/url': httpUrl,
'/proxy': httpUrl, '/proxy': httpUrl,
'/_info_card_': httpUrl,
'/bios': httpUrl,
'/cli': httpUrl,
}, },
}, },
build: { build: {

View file

@ -4119,6 +4119,7 @@ export type components = {
reactions: { reactions: {
[key: string]: number; [key: string]: number;
}; };
reactionCount: number;
renoteCount: number; renoteCount: number;
repliesCount: number; repliesCount: number;
uri?: string; uri?: string;

File diff suppressed because it is too large Load diff