Compare commits

...

81 commits

Author SHA1 Message Date
dakkar
7c688499c2 merge: some future changes coming from upstream (!465)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/465
2024-04-07 19:00:22 +00:00
Marie
bb7b4a8ea4 merge: fix: send null for empty edited_at in mastodon api (!487)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/487

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
2024-04-07 15:36:59 +00:00
dakkar
0690b9a429 merge: fix: load libopenmpt on demand (!469)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/469

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <marie@kaifa.ch>
2024-04-07 14:56:16 +00:00
Sugar🍬🍭🏳️‍⚧
e779c1e667 fix: send null for empty edited_at in mastodon api 2024-04-04 10:43:28 +02:00
Alina Sireneva
ecfaf7ff7a chore: added license and patch info 2024-03-14 21:39:34 +03:00
Alina Sireneva
a69315a24b fix: added wasm in vite config 2024-03-14 14:41:24 +03:00
Alina Sireneva
d991eccd3f fix: Promise.resolve 2024-03-11 16:42:10 +03:00
Alina Sireneva
0085305579 fix: load libopenmpt on demand 2024-03-11 15:32:59 +03: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
81 changed files with 2283 additions and 2107 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 @@
<!--
## 202x.x.x (unreleased)
## Unreleased
### General
-
### Client
-
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
- Enhance: リアクション・いいねの総数を表示するように
- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
### Server
-
-->
## 2024.3.1
### 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.
## 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
### Misskeyのドメイン固有の概念は`Mi`をprefixする

10
locales/index.d.ts vendored
View file

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

View file

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

View file

@ -51,21 +51,35 @@ export class FetchInstanceMetadataService {
}
@bindThis
public async tryLock(host: string): Promise<boolean> {
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
return mutex !== '1';
// public for test
public async tryLock(host: string): Promise<string | null> {
// 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
public unlock(host: string): Promise<'OK'> {
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
// public for test
public unlock(host: string): Promise<number> {
return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`);
}
@bindThis
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
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 {
if (!force) {
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, {
detail: true,
}) : null,
userId: opts.withUser ? file.userId : null,
userId: file.userId,
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,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,

View file

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

View file

@ -279,7 +279,8 @@ export class MastoConverters {
emoji_reactions: status.emoji_reactions,
bookmarked: false,
quote: isQuote ? await this.convertReblog(status.reblog) : false,
edited_at: note.updatedAt?.toISOString(),
// optional chaining cannot be used, as it evaluates to undefined, not null
edited_at: note.updatedAt ? note.updatedAt.toISOString() : null,
});
}
}

View file

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

View file

@ -43,7 +43,6 @@ html
link(rel='stylesheet' href='/assets/phosphor-icons/bold/style.css')
link(rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
script(src='/client-assets/libopenmpt.js')
if !config.clientManifestExists
script(type="module" src="/vite/@vite/client")
@ -73,7 +72,6 @@ html
script.
var VERSION = "#{version}";
var CLIENT_ENTRY = "#{clientEntry.file}";
window.libopenmpt = window.Module;
script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson

View file

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

View file

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

View file

@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
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';
describe('API visibility', () => {
@ -24,38 +24,38 @@ describe('API visibility', () => {
let target2: misskey.entities.SignupResponse;
/** public-post */
let pub: any;
let pub: misskey.entities.Note;
/** home-post */
let home: any;
let home: misskey.entities.Note;
/** followers-post */
let fol: any;
let fol: misskey.entities.Note;
/** specified-post */
let spe: any;
let spe: misskey.entities.Note;
/** public-reply to target's post */
let pubR: any;
let pubR: misskey.entities.Note;
/** home-reply to target's post */
let homeR: any;
let homeR: misskey.entities.Note;
/** followers-reply to target's post */
let folR: any;
let folR: misskey.entities.Note;
/** specified-reply to target's post */
let speR: any;
let speR: misskey.entities.Note;
/** public-mention to target */
let pubM: any;
let pubM: misskey.entities.Note;
/** home-mention to target */
let homeM: any;
let homeM: misskey.entities.Note;
/** followers-mention to target */
let folM: any;
let folM: misskey.entities.Note;
/** specified-mention to target */
let speM: any;
let speM: misskey.entities.Note;
/** reply target post */
let tgt: any;
let tgt: misskey.entities.Note;
//#endregion
const show = async (noteId: any, by: any) => {
return await api('/notes/show', {
const show = async (noteId: misskey.entities.Note['id'], by?: UserToken) => {
return await api('notes/show', {
noteId,
}, by);
};
@ -70,7 +70,7 @@ describe('API visibility', () => {
target2 = await signup({ username: 'target2' });
// follow alice <= follower
await api('/following/create', { userId: alice.id }, follower);
await api('following/create', { userId: alice.id }, follower);
// normal posts
pub = await post(alice, { text: 'x', visibility: 'public' });
@ -111,7 +111,7 @@ describe('API visibility', () => {
});
test('[show] public-postを未認証が見れる', async () => {
const res = await show(pub.id, null);
const res = await show(pub.id);
assert.strictEqual(res.body.text, 'x');
});
@ -132,7 +132,7 @@ describe('API visibility', () => {
});
test('[show] home-postを未認証が見れる', async () => {
const res = await show(home.id, null);
const res = await show(home.id);
assert.strictEqual(res.body.text, 'x');
});
@ -153,7 +153,7 @@ describe('API visibility', () => {
});
test('[show] followers-postを未認証が見れない', async () => {
const res = await show(fol.id, null);
const res = await show(fol.id);
assert.strictEqual(res.body.isHidden, true);
});
@ -179,7 +179,7 @@ describe('API visibility', () => {
});
test('[show] specified-postを未認証が見れない', async () => {
const res = await show(spe.id, null);
const res = await show(spe.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
@ -207,7 +207,7 @@ describe('API visibility', () => {
});
test('[show] public-replyを未認証が見れる', async () => {
const res = await show(pubR.id, null);
const res = await show(pubR.id);
assert.strictEqual(res.body.text, 'x');
});
@ -233,7 +233,7 @@ describe('API visibility', () => {
});
test('[show] home-replyを未認証が見れる', async () => {
const res = await show(homeR.id, null);
const res = await show(homeR.id);
assert.strictEqual(res.body.text, 'x');
});
@ -259,7 +259,7 @@ describe('API visibility', () => {
});
test('[show] followers-replyを未認証が見れない', async () => {
const res = await show(folR.id, null);
const res = await show(folR.id);
assert.strictEqual(res.body.isHidden, true);
});
@ -290,7 +290,7 @@ describe('API visibility', () => {
});
test('[show] specified-replyを未認証が見れない', async () => {
const res = await show(speR.id, null);
const res = await show(speR.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
@ -318,7 +318,7 @@ describe('API visibility', () => {
});
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');
});
@ -344,7 +344,7 @@ describe('API visibility', () => {
});
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');
});
@ -370,7 +370,7 @@ describe('API visibility', () => {
});
test('[show] followers-mentionを未認証が見れない', async () => {
const res = await show(folM.id, null);
const res = await show(folM.id);
assert.strictEqual(res.body.isHidden, true);
});
@ -401,28 +401,28 @@ describe('API visibility', () => {
});
test('[show] specified-mentionを未認証が見れない', async () => {
const res = await show(speM.id, null);
const res = await show(speM.id);
assert.strictEqual(res.body.isHidden, true);
});
//#endregion
//#region HTL
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);
const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes[0].text, 'x');
});
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);
const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes.length, 0);
});
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);
const notes = res.body.filter((n: any) => n.id === fol.id);
assert.strictEqual(notes[0].text, 'x');
@ -431,21 +431,21 @@ describe('API visibility', () => {
//#region RTL
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);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
});
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);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes.length, 0);
});
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);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
@ -454,14 +454,14 @@ describe('API visibility', () => {
//#region MTL
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);
const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x');
});
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);
const notes = res.body.filter((n: any) => n.id === folM.id);
assert.strictEqual(notes[0].text, '@target x');

View file

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

View file

@ -22,7 +22,7 @@ describe('Block', () => {
}, 1000 * 60 * 2);
test('Block作成', async () => {
const res = await api('/blocking/create', {
const res = await api('blocking/create', {
userId: bob.id,
}, alice);
@ -30,7 +30,7 @@ describe('Block', () => {
});
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.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
@ -39,7 +39,7 @@ describe('Block', () => {
test('ブロックされているユーザーにリアクションできない', async () => {
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.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
@ -48,7 +48,7 @@ describe('Block', () => {
test('ブロックされているユーザーに返信できない', async () => {
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.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -57,7 +57,7 @@ describe('Block', () => {
test('ブロックされているユーザーのートをRenoteできない', async () => {
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.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -72,12 +72,13 @@ describe('Block', () => {
const bobNote = await post(bob, { 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(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
assert.strictEqual(body.some(note => note.id === bobNote.id), true);
assert.strictEqual(body.some(note => note.id === carolNote.id), true);
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,7 +55,7 @@ describe('Account Move', () => {
}, 1000 * 10);
test('Able to create an alias', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
@ -67,7 +67,7 @@ describe('Account Move', () => {
});
test('Able to create a local alias without hostname', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: ['@alice'],
}, bob);
@ -77,7 +77,7 @@ describe('Account Move', () => {
});
test('Able to create a local alias without @', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: ['alice'],
}, bob);
@ -87,7 +87,7 @@ describe('Account Move', () => {
});
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'],
}, bob);
@ -97,7 +97,7 @@ describe('Account Move', () => {
});
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}`],
}, bob);
@ -107,7 +107,7 @@ describe('Account Move', () => {
});
test('Unable to add itself', async () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@bob@${url.hostname}`],
}, bob);
@ -117,7 +117,7 @@ describe('Account Move', () => {
});
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}`],
}, bob);
@ -125,7 +125,7 @@ describe('Account Move', () => {
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
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'],
}, bob);
@ -135,7 +135,7 @@ describe('Account Move', () => {
});
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}`],
}, bob);
@ -146,10 +146,10 @@ describe('Account Move', () => {
});
test('Able to properly overwrite alsoKnownAs', async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`],
}, bob);
@ -164,27 +164,27 @@ describe('Account Move', () => {
let antennaId = '';
beforeAll(async () => {
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, root);
const listRoot = await api('/users/lists/create', {
const listRoot = await api('users/lists/create', {
name: secureRndstr(8),
}, root);
await api('/users/lists/push', {
await api('users/lists/push', {
listId: listRoot.body.id,
userId: alice.id,
}, root);
await api('/following/create', {
await api('following/create', {
userId: root.id,
}, alice);
await api('/following/create', {
await api('following/create', {
userId: eve.id,
}, alice);
const antenna = await api('/antennas/create', {
const antenna = await api('antennas/create', {
name: secureRndstr(8),
src: 'home',
keywords: [secureRndstr(8)],
keywords: [[secureRndstr(8)]],
excludeKeywords: [],
users: [],
caseSensitive: false,
@ -195,48 +195,48 @@ describe('Account Move', () => {
}, alice);
antennaId = antenna.body.id;
await api('/i/update', {
await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, carol);
await api('/mute/create', {
await api('mute/create', {
userId: alice.id,
}, dave);
await api('/blocking/create', {
await api('blocking/create', {
userId: alice.id,
}, dave);
await api('/following/create', {
await api('following/create', {
userId: eve.id,
}, dave);
await api('/following/create', {
await api('following/create', {
userId: dave.id,
}, eve);
const listEve = await api('/users/lists/create', {
const listEve = await api('users/lists/create', {
name: secureRndstr(8),
}, eve);
await api('/users/lists/push', {
await api('users/lists/push', {
listId: listEve.body.id,
userId: bob.id,
}, eve);
await api('/i/update', {
await api('i/update', {
isLocked: true,
}, frank);
await api('/following/create', {
await api('following/create', {
userId: frank.id,
}, alice);
await api('/following/requests/accept', {
await api('following/requests/accept', {
userId: alice.id,
}, frank);
}, 1000 * 10);
test('Prohibit the root account from moving', async () => {
const res = await api('/i/move', {
const res = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`,
}, root);
@ -246,7 +246,7 @@ describe('Account Move', () => {
});
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}`,
}, alice);
@ -256,7 +256,7 @@ describe('Account Move', () => {
});
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}`,
}, alice);
@ -266,7 +266,7 @@ describe('Account Move', () => {
});
test('Relationships have been properly migrated', async () => {
const move = await api('/i/move', {
const move = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`,
}, alice);
@ -275,13 +275,13 @@ describe('Account Move', () => {
await sleep(1000 * 3); // wait for jobs to finish
// Unfollow delayed?
const aliceFollowings = await api('/users/following', {
const aliceFollowings = await api('users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(aliceFollowings.status, 200);
assert.strictEqual(aliceFollowings.body.length, 3);
const carolFollowings = await api('/users/following', {
const carolFollowings = await api('users/following', {
userId: carol.id,
}, carol);
assert.strictEqual(carolFollowings.status, 200);
@ -289,25 +289,25 @@ describe('Account Move', () => {
assert.strictEqual(carolFollowings.body[0].followeeId, bob.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.body.length, 2);
assert.strictEqual(blockings.body[0].blockeeId, bob.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.body.length, 2);
assert.strictEqual(mutings.body[0].muteeId, bob.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.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 === 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.body[0].userIds.length, 1);
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 () => {
await successfulApiCall({
endpoint: '/following/create',
endpoint: 'following/create',
parameters: {
userId: frank.id,
},
user: bob,
});
const followers = await api('/users/followers', {
const followers = await api('users/followers', {
userId: frank.id,
}, frank);
@ -333,7 +333,7 @@ describe('Account Move', () => {
test('Unfollowed after 10 sec (24 hours in production).', async () => {
await sleep(1000 * 8);
const following = await api('/users/following', {
const following = await api('users/following', {
userId: alice.id,
}, alice);
@ -342,7 +342,7 @@ describe('Account Move', () => {
});
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}`,
}, bob);
@ -352,7 +352,7 @@ describe('Account Move', () => {
});
test('Follow and follower counts are properly adjusted', async () => {
await api('/following/create', {
await api('following/create', {
userId: alice.id,
}, eve);
const newAlice = await Users.findOneByOrFail({ id: alice.id });
@ -365,7 +365,7 @@ describe('Account Move', () => {
assert.strictEqual(newEve.followingCount, 1);
assert.strictEqual(newEve.followersCount, 1);
await api('/following/delete', {
await api('following/delete', {
userId: alice.id,
}, eve);
newEve = await Users.findOneByOrFail({ id: eve.id });
@ -374,49 +374,49 @@ describe('Account Move', () => {
});
test.each([
'/antennas/create',
'/channels/create',
'/channels/favorite',
'/channels/follow',
'/channels/unfavorite',
'/channels/unfollow',
'/clips/add-note',
'/clips/create',
'/clips/favorite',
'/clips/remove-note',
'/clips/unfavorite',
'/clips/update',
'/drive/files/upload-from-url',
'/flash/create',
'/flash/like',
'/flash/unlike',
'/flash/update',
'/following/create',
'/gallery/posts/create',
'/gallery/posts/like',
'/gallery/posts/unlike',
'/gallery/posts/update',
'/i/claim-achievement',
'/i/move',
'/i/import-blocking',
'/i/import-following',
'/i/import-muting',
'/i/import-user-lists',
'/i/pin',
'/mute/create',
'/notes/create',
'/notes/favorites/create',
'/notes/polls/vote',
'/notes/reactions/create',
'/pages/create',
'/pages/like',
'/pages/unlike',
'/pages/update',
'/renote-mute/create',
'/users/lists/create',
'/users/lists/pull',
'/users/lists/push',
])('Prohibit access after moving: %s', async (endpoint) => {
'antennas/create',
'channels/create',
'channels/favorite',
'channels/follow',
'channels/unfavorite',
'channels/unfollow',
'clips/add-note',
'clips/create',
'clips/favorite',
'clips/remove-note',
'clips/unfavorite',
'clips/update',
'drive/files/upload-from-url',
'flash/create',
'flash/like',
'flash/unlike',
'flash/update',
'following/create',
'gallery/posts/create',
'gallery/posts/like',
'gallery/posts/unlike',
'gallery/posts/update',
'i/claim-achievement',
'i/move',
'i/import-blocking',
'i/import-following',
'i/import-muting',
'i/import-user-lists',
'i/pin',
'mute/create',
'notes/create',
'notes/favorites/create',
'notes/polls/vote',
'notes/reactions/create',
'pages/create',
'pages/like',
'pages/unlike',
'pages/update',
'renote-mute/create',
'users/lists/create',
'users/lists/pull',
'users/lists/push',
] as const)('Prohibit access after moving: %s', async (endpoint) => {
const res = await api(endpoint, {}, alice);
assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
@ -424,11 +424,11 @@ describe('Account Move', () => {
});
test('Prohibit access after moving: /antennas/update', async () => {
const res = await api('/antennas/update', {
const res = await api('antennas/update', {
antennaId,
name: secureRndstr(8),
src: 'users',
keywords: [secureRndstr(8)],
keywords: [[secureRndstr(8)]],
excludeKeywords: [],
users: [eve.id],
caseSensitive: false,
@ -447,12 +447,12 @@ describe('Account Move', () => {
const res = await uploadFile(alice);
assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.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.code, 'YOUR_ACCOUNT_MOVED');
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 () => {
const res = await api('/i/update', {
const res = await api('i/update', {
alsoKnownAs: [`@eve@${url.hostname}`],
}, alice);

View file

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

View file

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

View file

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

View file

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

View file

@ -24,12 +24,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root 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 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(Array.isArray(res.body), true);
@ -40,15 +40,15 @@ describe('Note thread mute', () => {
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' });
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 res = await api('/i', {}, alice);
const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
@ -56,11 +56,11 @@ describe('Note thread mute', () => {
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' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
let fired = false;
@ -84,12 +84,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root 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 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(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', () => {
let alice: misskey.entities.SignupResponse;
let jpgNote: any;
let pngNote: any;
let jpgPngNote: any;
let jpgNote: misskey.entities.Note;
let pngNote: misskey.entities.Note;
let jpgPngNote: misskey.entities.Note;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
@ -31,7 +31,7 @@ describe('users/notes', () => {
}, 1000 * 60 * 2);
test('withFiles', async () => {
const res = await api('/users/notes', {
const res = await api('users/notes', {
userId: alice.id,
withFiles: true,
}, alice);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

@ -189,14 +189,26 @@ export async function mainBoot() {
if ($i.followersCount >= 500) claimAchievement('followers500');
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');
}
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) {
@ -231,7 +243,7 @@ export async function mainBoot() {
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
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)))) {
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 { hms } from '@/filters/hms.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
import { iAmModerator } from '@/account.js';
import { $i, iAmModerator } from '@/account.js';
const props = defineProps<{
audio: Misskey.entities.DriveFile;
@ -99,8 +99,6 @@ function showMenu(ev: MouseEvent) {
if (iAmModerator) {
menu.push({
type: 'divider',
}, {
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',
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;
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right',

View file

@ -60,7 +60,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { iAmModerator } from '@/account.js';
import { $i, iAmModerator } from '@/account.js';
const props = withDefaults(defineProps<{
image: Misskey.entities.DriveFile;
@ -115,6 +115,13 @@ function showMenu(ev: MouseEvent) {
action: () => {
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);
}

View file

@ -97,7 +97,7 @@ import * as os from '@/os.js';
import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
import hasAudio from '@/scripts/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
import { iAmModerator } from '@/account.js';
import { $i, iAmModerator } from '@/account.js';
const props = defineProps<{
video: Misskey.entities.DriveFile;
@ -125,8 +125,6 @@ function showMenu(ev: MouseEvent) {
if (iAmModerator) {
menu.push({
type: 'divider',
}, {
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',
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;
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right',

View file

@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</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>
</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>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<footer :class="$style.footer">
<button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<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
v-if="canRenote"
@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
>
<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 v-else :class="$style.footerButton" class="_button" disabled>
<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()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<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>
</button>
<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>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<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 { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import * as os from '@/os.js';
import * as sound from '@/scripts/sound.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 {
if (props.mock) {
return;

View file

@ -113,10 +113,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
</MkA>
</div>
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
<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
v-if="canRenote"
@ -127,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote() : boostVisibility()"
>
<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 v-else class="_button" :class="$style.noteFooterButton" disabled>
<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()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
<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>
</button>
<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>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
<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 { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js';
import number from '@/filters/number.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as sound from '@/scripts/sound.js';
@ -594,11 +595,11 @@ function like(): void {
}
}
function undoReact(note): void {
const oldReaction = note.myReaction;
function undoReact(targetNote: Misskey.entities.Note): void {
const oldReaction = targetNote.myReaction;
if (!oldReaction) return;
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 {
const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true;

View file

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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-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 === '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"/>
@ -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 === '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>
<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 === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
@ -211,6 +213,7 @@ const rejectFollowRequest = () => {
}
.icon_reactionGroup,
.icon_reactionGroupHeart,
.icon_renoteGroup {
display: grid;
align-items: center;
@ -223,11 +226,15 @@ const rejectFollowRequest = () => {
}
.icon_reactionGroup {
background: #e99a0b;
background: var(--eventReaction);
}
.icon_reactionGroupHeart {
background: var(--eventReactionHeart);
}
.icon_renoteGroup {
background: #36d298;
background: var(--eventRenote);
}
.icon_app {
@ -256,49 +263,49 @@ const rejectFollowRequest = () => {
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest {
padding: 3px;
background: #36aed2;
background: var(--eventFollow);
pointer-events: none;
}
.t_renote {
padding: 3px;
background: #36d298;
background: var(--eventRenote);
pointer-events: none;
}
.t_quote {
padding: 3px;
background: #36d298;
background: var(--eventRenote);
pointer-events: none;
}
.t_reply {
padding: 3px;
background: #007aff;
background: var(--eventReply);
pointer-events: none;
}
.t_mention {
padding: 3px;
background: #88a6b7;
background: var(--eventOther);
pointer-events: none;
}
.t_pollEnded {
padding: 3px;
background: #88a6b7;
background: var(--eventOther);
pointer-events: none;
}
.t_achievementEarned {
padding: 3px;
background: #cb9a11;
background: var(--eventAchievement);
pointer-events: none;
}
.t_roleAssigned {
padding: 3px;
background: #88a6b7;
background: var(--eventOther);
pointer-events: none;
}

View file

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

View file

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

View file

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

View file

@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</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>
</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>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
@ -107,7 +107,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<footer :class="$style.footer">
<button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<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
v-if="canRenote"
@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
>
<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 v-else :class="$style.footerButton" class="_button" disabled>
<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()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<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>
</button>
<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>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<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 { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.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 {
if (props.mock) {
return;

View file

@ -120,11 +120,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
</MkA>
</div>
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
<footer :class="$style.footer">
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
<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
v-if="canRenote"
@ -135,7 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote() : boostVisibility()"
>
<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 v-else class="_button" :class="$style.noteFooterButton" disabled>
<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()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<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>
</button>
<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>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
<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 { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
@ -603,11 +604,11 @@ function like(): void {
}
}
function undoReact(note): void {
const oldReaction = note.myReaction;
function undoReact(targetNote: Misskey.entities.Note): void {
const oldReaction = targetNote.myReaction;
if (!oldReaction) return;
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 {
const isLink = (el: HTMLElement): boolean => {
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',
}]"
>
<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">
<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 v-else :class="$style.menu">
<div :class="$style.menuContainer">
@ -32,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { i18n } from '@/i18n.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 { defaultStore } from '@/store.js';
import * as os from '@/os.js';
@ -96,6 +106,9 @@ const choseAd = (): Ad | null => {
};
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));
function reduceFrequency(): void {

View file

@ -373,7 +373,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
this.currentRoute.value = res.route;
this.currentKey = res.route.globalCacheKey ?? key ?? path;
if (emitChange) {
if (emitChange && res.route.path !== '/:(*)') {
this.emit('change', {
beforePath,
path,
@ -408,6 +408,9 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
if (cancel) return;
}
const res = this.navigate(path, null);
if (res.route.path === '/:(*)') {
location.href = path;
} else {
this.emit('push', {
beforePath,
path: res._parsedRoute.fullPath,
@ -416,6 +419,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
key: this.currentKey,
});
}
}
public replace(path: string, key?: string | null) {
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-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</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="loadRawImages">{{ i18n.ts.loadRawImages }}</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 animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));

View file

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

View file

@ -1,9 +1,12 @@
/* global libopenmpt UTF8ToString writeAsciiToMemory */
// @ts-nocheck
/* eslint-disable */
const ChiptuneAudioContext = window.AudioContext || window.webkitAudioContext;
export function ChiptuneJsConfig (repeatCount: number, context: AudioContext) {
let libopenmpt
let libopenmptLoadPromise
export function ChiptuneJsConfig (repeatCount?: number, context?: AudioContext) {
this.repeatCount = repeatCount;
this.context = context;
}
@ -20,6 +23,28 @@ export function ChiptuneJsPlayer (config: object) {
this.volume = 1;
}
ChiptuneJsPlayer.prototype.initialize = function() {
if (libopenmptLoadPromise) return libopenmptLoadPromise;
if (libopenmpt) return Promise.resolve();
libopenmptLoadPromise = new Promise(async (resolve, reject) => {
try {
const { Module } = await import('./libopenmpt/libopenmpt.js');
await new Promise((resolve) => {
Module['onRuntimeInitialized'] = resolve;
})
libopenmpt = Module;
resolve()
} catch (e) {
reject(e)
} finally {
libopenmptLoadPromise = undefined;
}
})
return libopenmptLoadPromise;
}
ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer;
ChiptuneJsPlayer.prototype.fireEvent = function (eventName: string, response) {
@ -61,12 +86,12 @@ ChiptuneJsPlayer.prototype.seek = function (position: number) {
ChiptuneJsPlayer.prototype.metadata = function () {
const data = {};
const keys = UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';');
const keys = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';');
let keyNameBuffer = 0;
for (const key of keys) {
keyNameBuffer = libopenmpt._malloc(key.length + 1);
writeAsciiToMemory(key, keyNameBuffer);
data[key] = UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer));
libopenmpt.writeAsciiToMemory(key, keyNameBuffer);
data[key] = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer));
libopenmpt._free(keyNameBuffer);
}
return data;
@ -84,7 +109,7 @@ ChiptuneJsPlayer.prototype.unlock = function () {
};
ChiptuneJsPlayer.prototype.load = function (input) {
return new Promise((resolve, reject) => {
return this.initialize().then(() => new Promise((resolve, reject) => {
if(this.touchLocked) {
this.unlock();
}
@ -106,7 +131,7 @@ ChiptuneJsPlayer.prototype.load = function (input) {
reject(error);
});
}
});
}));
};
ChiptuneJsPlayer.prototype.play = function (buffer: ArrayBuffer) {
@ -180,7 +205,7 @@ ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) {
ChiptuneJsPlayer.prototype.getPatternRowChannel = function (pattern: number, row: number, channel: number) {
if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) {
return UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true));
return libopenmpt.UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true));
}
return '';
};

View file

@ -0,0 +1,25 @@
Copyright (c) 2004-2024, OpenMPT Project Developers and Contributors
Copyright (c) 1997-2003, Olivier Lapicque
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the OpenMPT project nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,23 @@
modifications made to `libopenmpt.js` (can be taken from https://lib.openmpt.org/libopenmpt/download/):
at the beginning of the file:
```js
// @ts-nocheck
/* eslint-disable */
```
at the end of the file:
```js
Module.UTF8ToString = UTF8ToString;
Module.writeAsciiToMemory = writeAsciiToMemory;
export { Module }
```
replace
```
wasmBinaryFile="libopenmpt.wasm"
```
with
```
wasmBinaryFile=new URL("./libopenmpt.wasm", import.meta.url).href
```

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import meta from '../../package.json';
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.js';
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue', '.wasm'];
const hash = (str: string, seed = 0): number => {
let h1 = 0xdeadbeef ^ seed,

View file

@ -19,6 +19,7 @@ namespace Entity {
content: string
plain_content?: string | null
created_at: string
edited_at: string | null
emojis: Emoji[]
replies_count: number
reblogs_count: number

View file

@ -725,6 +725,7 @@ namespace FriendicaAPI {
content: s.content,
plain_content: null,
created_at: s.created_at,
edited_at: s.edited_at || null,
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
replies_count: s.replies_count,
reblogs_count: s.reblogs_count,

View file

@ -17,6 +17,7 @@ namespace FriendicaEntity {
reblog: Status | null
content: string
created_at: string
edited_at?: string | null
emojis: Emoji[]
replies_count: number
reblogs_count: number

View file

@ -628,6 +628,7 @@ namespace MastodonAPI {
content: s.content,
plain_content: null,
created_at: s.created_at,
edited_at: s.edited_at || null,
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
replies_count: s.replies_count,
reblogs_count: s.reblogs_count,

View file

@ -18,6 +18,7 @@ namespace MastodonEntity {
reblog: Status | null
content: string
created_at: string
edited_at?: string | null
emojis: Emoji[]
replies_count: number
reblogs_count: number

View file

@ -283,6 +283,7 @@ namespace MisskeyAPI {
: '',
plain_content: n.text ? n.text : null,
created_at: n.createdAt,
edited_at: n.updatedAt || null,
emojis: mapEmojis(n.emojis).concat(mapReactionEmojis(n.reactionEmojis)),
replies_count: n.repliesCount,
reblogs_count: n.renoteCount,

View file

@ -7,6 +7,7 @@ namespace MisskeyEntity {
export type Note = {
id: string
createdAt: string
updatedAt?: string | null
userId: string
user: User
text: string | null

View file

@ -357,6 +357,7 @@ namespace PleromaAPI {
content: s.content,
plain_content: s.pleroma.content?.['text/plain'] ? s.pleroma.content['text/plain'] : null,
created_at: s.created_at,
edited_at: s.edited_at || null,
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
replies_count: s.replies_count,
reblogs_count: s.reblogs_count,

View file

@ -18,6 +18,7 @@ namespace PleromaEntity {
reblog: Status | null
content: string
created_at: string
edited_at?: string | null
emojis: Emoji[]
replies_count: number
reblogs_count: number

View file

@ -49,6 +49,7 @@ const status: Entity.Status = {
content: 'hoge',
plain_content: null,
created_at: '2019-03-26T21:40:32',
edited_at: null,
emojis: [],
replies_count: 0,
reblogs_count: 0,

View file

@ -38,6 +38,7 @@ const status: Entity.Status = {
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
edited_at: null,
emojis: [],
replies_count: 0,
reblogs_count: 0,

View file

@ -37,6 +37,7 @@ const status: Entity.Status = {
content: 'hoge',
plain_content: 'hoge',
created_at: '2019-03-26T21:40:32',
edited_at: null,
emojis: [],
replies_count: 0,
reblogs_count: 0,

View file

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

File diff suppressed because it is too large Load diff