diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-03-22 09:55:38 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-22 09:55:38 +0900 |
| commit | 1e67e9c6616c6e87ae85ece71e5401006df2dd34 (patch) | |
| tree | a0d6df03a3d0ac2edf1fda7ed4bfb789b5a29720 | |
| parent | Merge pull request #10218 from misskey-dev/develop (diff) | |
| parent | fix drive-cleaner (diff) | |
| download | misskey-1e67e9c6616c6e87ae85ece71e5401006df2dd34.tar.gz misskey-1e67e9c6616c6e87ae85ece71e5401006df2dd34.tar.bz2 misskey-1e67e9c6616c6e87ae85ece71e5401006df2dd34.zip | |
Merge pull request #10342 from misskey-dev/develop
Release: 13.10.0
Diffstat (limited to '')
330 files changed, 8230 insertions, 2405 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE/01_bug.md b/.github/PULL_REQUEST_TEMPLATE/01_bug.md index 79ca97dfa0..0739fee709 100644 --- a/.github/PULL_REQUEST_TEMPLATE/01_bug.md +++ b/.github/PULL_REQUEST_TEMPLATE/01_bug.md @@ -4,14 +4,20 @@ Thank you for your PR! Before creating a PR, please check the contribution guide https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md --> -# What +## What <!-- ã“ã®PRã§ä½•ã‚’ã—ãŸã®ã‹ï¼Ÿ ã©ã†å¤‰ã‚ã‚‹ã®ã‹ï¼Ÿ --> <!-- What did you do with this PR? How will it change things? --> -# Why +## Why <!-- ãªãœãã†ã™ã‚‹ã®ã‹ï¼Ÿ ã©ã†ã„ã†æ„図ãªã®ã‹ï¼Ÿ 何ãŒå›°ã£ã¦ã„ã‚‹ã®ã‹ï¼Ÿ --> <!-- Why do you do it? What are your intentions? What is the problem? --> -# Additional info (optional) +## Additional info (optional) <!-- テスト観点ãªã© --> <!-- Test perspective, etc --> + +## Checklist +- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) +- [ ] Test working in a local environment +- [ ] (If needed) Update CHANGELOG.md +- [ ] (If possible) Add tests diff --git a/.github/PULL_REQUEST_TEMPLATE/02_enhance.md b/.github/PULL_REQUEST_TEMPLATE/02_enhance.md index 79ca97dfa0..0739fee709 100644 --- a/.github/PULL_REQUEST_TEMPLATE/02_enhance.md +++ b/.github/PULL_REQUEST_TEMPLATE/02_enhance.md @@ -4,14 +4,20 @@ Thank you for your PR! Before creating a PR, please check the contribution guide https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md --> -# What +## What <!-- ã“ã®PRã§ä½•ã‚’ã—ãŸã®ã‹ï¼Ÿ ã©ã†å¤‰ã‚ã‚‹ã®ã‹ï¼Ÿ --> <!-- What did you do with this PR? How will it change things? --> -# Why +## Why <!-- ãªãœãã†ã™ã‚‹ã®ã‹ï¼Ÿ ã©ã†ã„ã†æ„図ãªã®ã‹ï¼Ÿ 何ãŒå›°ã£ã¦ã„ã‚‹ã®ã‹ï¼Ÿ --> <!-- Why do you do it? What are your intentions? What is the problem? --> -# Additional info (optional) +## Additional info (optional) <!-- テスト観点ãªã© --> <!-- Test perspective, etc --> + +## Checklist +- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) +- [ ] Test working in a local environment +- [ ] (If needed) Update CHANGELOG.md +- [ ] (If possible) Add tests diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..0739fee709 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,23 @@ +<!-- ℹ ãŠèªã¿ãã ã•ã„ / README +PRã‚りãŒã¨ã†ã”ã–ã„ã¾ã™ï¼ PRを作æˆã™ã‚‹å‰ã«ã€ã‚³ãƒ³ãƒˆãƒªãƒ“ューションガイドをã”確èªãã ã•ã„: +Thank you for your PR! Before creating a PR, please check the contribution guide: +https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md +--> + +## What +<!-- ã“ã®PRã§ä½•ã‚’ã—ãŸã®ã‹ï¼Ÿ ã©ã†å¤‰ã‚ã‚‹ã®ã‹ï¼Ÿ --> +<!-- What did you do with this PR? How will it change things? --> + +## Why +<!-- ãªãœãã†ã™ã‚‹ã®ã‹ï¼Ÿ ã©ã†ã„ã†æ„図ãªã®ã‹ï¼Ÿ 何ãŒå›°ã£ã¦ã„ã‚‹ã®ã‹ï¼Ÿ --> +<!-- Why do you do it? What are your intentions? What is the problem? --> + +## Additional info (optional) +<!-- テスト観点ãªã© --> +<!-- Test perspective, etc --> + +## Checklist +- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) +- [ ] Test working in a local environment +- [ ] (If needed) Update CHANGELOG.md +- [ ] (If possible) Add tests diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml new file mode 100644 index 0000000000..44b6b4ba7e --- /dev/null +++ b/.github/workflows/test-backend.yml @@ -0,0 +1,59 @@ +name: Test (backend) + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + jest: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] + + services: + postgres: + image: postgres:13 + ports: + - 54312:5432 + env: + POSTGRES_DB: test-misskey + POSTGRES_HOST_AUTH_METHOD: trust + redis: + image: redis:6 + ports: + - 56312:6379 + + steps: + - uses: actions/checkout@v3.3.0 + with: + submodules: true + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.6.0 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Copy Configure + run: cp .github/misskey/test.yml .config + - name: Build + run: pnpm build + - name: Test + run: pnpm jest-and-coverage + - name: Upload Coverage + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./packages/backend/coverage/coverage-final.json diff --git a/.github/workflows/test.yml b/.github/workflows/test-frontend.yml index 9135b4f60a..18c1a31aee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test-frontend.yml @@ -1,4 +1,4 @@ -name: Test +name: Test (frontend) on: push: @@ -8,26 +8,13 @@ on: pull_request: jobs: - jest: + vitest: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x] - services: - postgres: - image: postgres:13 - ports: - - 54312:5432 - env: - POSTGRES_DB: test-misskey - POSTGRES_HOST_AUTH_METHOD: trust - redis: - image: redis:6 - ports: - - 56312:6379 - steps: - uses: actions/checkout@v3.3.0 with: @@ -51,12 +38,12 @@ jobs: - name: Build run: pnpm build - name: Test - run: pnpm jest-and-coverage + run: pnpm --filter frontend test-and-coverage - name: Upload Coverage uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./packages/backend/coverage/coverage-final.json + files: ./packages/frontend/coverage/coverage-final.json e2e: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index f067de2926..c5fe52bc80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,71 @@ <!-- ## 13.x.x (unreleased) -### Improvements -- +### General +- -### Bugfixes -x +### Client +- + +### Server +- -You should also include the user name that made the change. --> +## 13.10.0 + +### General +- ユーザーã”ã¨ã«Renoteをミュートã§ãるよã†ã« +- ノートã”ã¨ã«çµµæ–‡å—リアクションをå—ã‘å–ã‚‹ã‹è¨å®šã§ãるよã†ã« +- ã‚¯ãƒªãƒƒãƒ—ã‚’ãŠæ°—ã«å…¥ã‚Šã«ç™»éŒ²ã§ãるよã†ã« +- ノート検索ã®åˆ©ç”¨å¯å¦ã‚’ãƒãƒ¼ãƒ«ã§åˆ¶å¾¡å¯èƒ½ã«(デフォルトã§ã‚ªãƒ•) +- ãƒãƒ¼ãƒ«ã®ä¸¦ã³é †ã‚’è¨å®šå¯èƒ½ã« +- カスタム絵文å—ã«ãƒ©ã‚¤ã‚»ãƒ³ã‚¹æƒ…å ±ã‚’ä»˜ä¸Žã§ãるよã†ã« +- 指定ã—ãŸæ–‡å—列をå«ã‚€æŠ•稿ã®å…¬é–‹ç¯„囲をホームã«ã§ãるよã†ã« +- 使ã‚れã¦ãªã„アンテナã¯è‡ªå‹•åœæ¢ã•れるよã†ã« + +### Client +- è¨å®šã‹ã‚‰è‡ªåˆ†ã®ãƒãƒ¼ãƒ«ã‚’確èªã§ãるよã†ã« +- åºƒå‘Šä¸€è¦§ãƒšãƒ¼ã‚¸ã‚’è¿½åŠ +- ãƒ‰ãƒ©ã‚¤ãƒ–ã‚¯ãƒªãƒ¼ãƒŠãƒ¼ã‚’è¿½åŠ +- DMä½œæˆæ™‚ã«ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã‚‚å«ã‚€ã‚ˆã†ã« +- フォãƒãƒ¼ç”³è«‹ã®ãƒœã‚¿ãƒ³ã®ãƒ‡ã‚¶ã‚¤ãƒ³ã‚’改善 +- 付箋ウィジェットã®é«˜ã•ã‚’è¨å®šå¯èƒ½ã« +- APオブジェクトを入力ã—ã¦ãƒ•ェッãƒã™ã‚‹æ©Ÿèƒ½ã¨ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚„ãƒŽãƒ¼ãƒˆã®æ¤œç´¢æ©Ÿèƒ½ã‚’分離 +- ナビゲーションãƒãƒ¼ã®é …ç›®ã«ã€Œãƒ—ãƒãƒ•ィールã€ã‚’è¿½åŠ ã§ãるよã†ã« +- ナビゲーションãƒãƒ¼ã®ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºã‚’ドラッグ&ドãƒãƒƒãƒ—ã§è¡Œãˆã‚‹ã‚ˆã†ã« +- ジョブã‚ューã®å†è©¦è¡Œã‚’ワンクリックã§ã§ãるよã†ã« +- AiScriptã‚’0.13.1ã«æ›´æ–° +- oEmbedをサãƒãƒ¼ãƒˆã—ã¦ã„るウェブサイトã®ãƒ—レビューãŒã§ãるよã†ã« + - YouTubeã‚’oEmbedã§ãƒãƒ¼ãƒ‰ã—ã€ãƒ—レビューã§å…±æœ‰ãƒœã‚¿ãƒ³ã‚’押ã™ã¨OSã®å…±æœ‰ç”»é¢ãŒã§ã‚‹ã‚ˆã†ã« + - ([Firefoxã§Spotifyã®ãƒ—レビューを開ã‘ã‚‹ã¨ãƒ•ルサイズã˜ã‚ƒãªãプレビューサイズã ã‘å†ç”Ÿã§ãã‚‹å•題](https://bugzilla.mozilla.org/show_bug.cgi?id=1792395)ãŒã‚りã¾ã™) + - (ã™ã§ã«ãƒ–ラウザーã§ã‚ャッシュã•れãŸãƒªãƒ³ã‚¯ã«å¯¾ã—ã¦ã¯ä»¥å‰ã®ãƒ—レビュー行動ãŒè¡Œã‚れã¦ã¾ã™ã€‚ãã®å ´åˆã€ãƒ–ラウザーã®ã‚ャッシュをクリアã—ã¦ã¾ãŸè©¦ã—ã¦ãã ã•ã„。) +- プãƒãƒ•ィールã§è¨å®šã—ãŸæƒ…å ±ãŒå‰Šé™¤ã§ããªã„å•é¡Œã‚’ä¿®æ£ +- ãƒãƒ¼ãƒ«ã§åºƒå‘Šã‚’無効ã«ã™ã‚‹ã¨admin/adsã§ãƒ—レビューãŒã§ã¦ã“ãªã„å•é¡Œã‚’ä¿®æ£ +- /api-consoleページã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã¨404ãŒå‡ºã‚‹å•é¡Œã‚’ä¿®æ£ +- Safariã§ãƒ—ラグインãŒè¤‡æ•°ã‚ã‚‹å ´åˆã«æ£å¸¸ã«èªã¿è¾¼ã¾ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- Bookwyrmã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ—ãƒãƒ•ィールページã§ã€Œãƒªãƒ¢ãƒ¼ãƒˆã§è¡¨ç¤ºã€ã‚’タップã—ã¦ã‚‚å応ãŒãªã„å•é¡Œã‚’ä¿®æ£ +- éžãƒã‚°ã‚¤ãƒ³æ™‚ã®ã€ŒMisskeyã«ã¤ã„ã¦ã€ã®è¡¨ç¤ºã‚’ä¿®æ£ +- PC版ã«ã¦ã€Œè¨å®šã€ã€Œã‚³ãƒ³ãƒˆãƒãƒ¼ãƒ«ãƒ‘ãƒãƒ«ã€ã®ãƒªãƒ³ã‚¯ã‚’2度以上続ã‘ã¦ã‚¯ãƒªãƒƒã‚¯ã—ãŸéš›ã«ç©ºç™½ã®ãƒšãƒ¼ã‚¸ãŒè¡¨ç¤ºã•れるå•é¡Œã‚’ä¿®æ£ + +### Server +- OpenAPIエンドãƒã‚¤ãƒ³ãƒˆã‚’復旧 +- WebP/AVIF/JPEGã®web公開用画åƒã¯ã€ã‚µãƒ¼ãƒãƒ¼ã‚µã‚¤ãƒ‰ã§ã¯JPEGã§ã¯ãªãWebPã«å¤‰æ›ã™ã‚‹ã‚ˆã†ã« +- アニメーション画åƒã®ã‚µãƒ ãƒã‚¤ãƒ«ã‚’生æˆã™ã‚‹ã‚ˆã†ã« +- アクティブユーザー数ãƒãƒ£ãƒ¼ãƒˆã®è¨˜éŒ²ä¸Šé™å€¤ã‚’æ‹¡å¼µ +- Playã®ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ä¸Šé™æ–‡å—æ•°ã‚’2å€ã«æ‹¡å¼µ +- é…é€å…ˆã‚µãƒ¼ãƒãƒ¼ãŒ410 Goneã§å¿œç”ã—ã¦ããŸå ´åˆã¯è‡ªå‹•ã§é…é€åœæ¢ã‚’ã™ã‚‹ã‚ˆã†ã« +- avatarBlurHash/bannerBlurHashã®åž‹ã‚’stringã«é™å®š +- タイムラインå–得時ã®ãƒ‘フォーマンスを改善 +- SMTP Login id length is too short +- API上ã§`visibility`ã‚’`followers`ã«è¨å®šã—ã¦renoteã™ã‚‹ã¨é€£åˆã‚„削除ã§ä¸å…·åˆãŒç™ºç”Ÿã™ã‚‹å•é¡Œã‚’ä¿®æ£ +- AWS S3ã‹ã‚‰ã®ãƒ•ァイル削除ã§NoSuchKeyエラーãŒå‡ºã‚‹ã¨é€²ã‚らãªã„状態ã«ãªã‚‹å•é¡Œã‚’ä¿®æ£ +- `disableCache: true`ã‚’è¨å®šã—ã¦ã„ã‚‹å ´åˆã«çµµæ–‡å—ç®¡ç†æ“作ã§ã‚¨ãƒ©ãƒ¼ãŒå‡ºã‚‹å•é¡Œã‚’ä¿®æ£ +- リテンション分æžãŒä¸Šæ‰‹ã機能ã—ãªã„ã“ã¨ãŒã‚ã‚‹ã®ã‚’ä¿®æ£ +- 空ã®ã‚¢ãƒ³ãƒ†ãƒŠãŒä½œæˆã§ããªã„よã†ã«ä¿®æ£ +- ç‰¹å®šã®æ¡ä»¶ã§é€šå ±ãŒè¦‹ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- 絵文å—ã®åå‰ã«ä»»æ„ã®æ–‡å—ãŒä½¿ç”¨ã§ãã‚‹å•é¡Œã‚’ä¿®æ£ + ## 13.9.2 (2023/03/06) ### Improvements @@ -246,8 +302,8 @@ You should also include the user name that made the change. ## 13.3.2 (2023/02/04) ### Improvements -- 外部メディアプãƒã‚ã‚·ã¸ã®å¯¾å¿œã‚’強化ã—ã¾ã—㟠- 外部メディアプãƒã‚ã‚·ã®Fastify実装を作りã¾ã—㟠+- 外部メディアプãƒã‚ã‚·ã¸ã®å¯¾å¿œã‚’強化ã—ã¾ã—㟠+ 外部メディアプãƒã‚ã‚·ã®Fastify実装を作りã¾ã—㟠https://github.com/misskey-dev/media-proxy - Server: improve performance @@ -410,7 +466,7 @@ You should also include the user name that made the change. - ユーザーã”ã¨ã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡è¨å®šã¯ãƒãƒ¼ãƒ«ã«çµ±åˆã•れã¾ã—ãŸã€‚ - インスタンスデフォルトã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡è¨å®šã¯ãƒãƒ¼ãƒ«ã«çµ±åˆã•れã¾ã—ãŸã€‚アップデート後ã€ãƒ™ãƒ¼ã‚¹ãƒãƒ¼ãƒ«ã‚‚ã—ãã¯ã‚³ãƒ³ãƒ‡ã‚£ã‚·ãƒ§ãƒŠãƒ«ãƒãƒ¼ãƒ«ã§ãƒ‰ãƒ©ã‚¤ãƒ–容é‡ã‚’編集ã—ã¦ãã ã•ã„。 - LTL/GTLã®è§£æ”¾çŠ¶æ…‹ã¯ãƒãƒ¼ãƒ«ã«çµ±åˆã•れã¾ã—ãŸã€‚ -- Dockerã®å®Ÿè¡Œã‚’rootã§è¡Œã‚ãªã„よã†ã«ã—ã¾ã—ãŸã€‚Dockerã‹ã¤ã‚ªãƒ–ジェクトストレージを使用ã—ã¦ã„ãªã„å ´åˆã¯`chown -hR 991.991 ./files`を実行ã—ã¦ãã ã•ã„。 +- Dockerã®å®Ÿè¡Œã‚’rootã§è¡Œã‚ãªã„よã†ã«ã—ã¾ã—ãŸã€‚Dockerã‹ã¤ã‚ªãƒ–ジェクトストレージを使用ã—ã¦ã„ãªã„å ´åˆã¯`chown -hR 991.991 ./files`を実行ã—ã¦ãã ã•ã„。 https://github.com/misskey-dev/misskey/pull/9560 #### For users @@ -638,7 +694,7 @@ You should also include the user name that made the change. ## 12.112.2 (2022/07/08) ### Bugfixes -- Fix Docker doesn't work @mei23 +- Fix Docker doesn't work @mei23 Still not working on arm64 environment. (See 12.112.0) ## 12.112.1 (2022/07/07) @@ -680,7 +736,7 @@ same as 12.112.0 - Improve player detection in URL preview @mei23 - Add Badge Image to Push Notification #8012 @tamaina - Server: Improve performance -- Server: Supports IPv6 on Redis transport. @mei23 +- Server: Supports IPv6 on Redis transport. @mei23 IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`. - Server: Add possibility to log IP addresses of users @syuilo - Add additional drive capacity change support @CyberRex0 diff --git a/cypress/e2e/basic.cy.js b/cypress/e2e/basic.cy.js index b1b856119c..8dc07c1800 100644 --- a/cypress/e2e/basic.cy.js +++ b/cypress/e2e/basic.cy.js @@ -52,13 +52,30 @@ describe('After setup instance', () => { cy.intercept('POST', '/api/signup').as('signup'); cy.get('[data-cy-signup]').click(); + cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-username] input').type('alice'); + cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-password] input').type('alice1234'); + cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-password-retype] input').type('alice1234'); + cy.get('[data-cy-signup-submit]').should('not.be.disabled'); cy.get('[data-cy-signup-submit]').click(); cy.wait('@signup'); }); + + it('signup with duplicated username', () => { + cy.registerUser('alice', 'alice1234'); + + cy.visitHome(); + + // ユーザーåãŒé‡è¤‡ã—ã¦ã„ã‚‹å ´åˆã®æŒ™å‹•ç¢ºèª + cy.get('[data-cy-signup]').click(); + cy.get('[data-cy-signup-username] input').type('alice'); + cy.get('[data-cy-signup-password] input').type('alice1234'); + cy.get('[data-cy-signup-password-retype] input').type('alice1234'); + cy.get('[data-cy-signup-submit]').should('be.disabled'); + }); }); describe('After user signup', () => { diff --git a/cypress/e2e/widgets.cy.js b/cypress/e2e/widgets.cy.js index 7d2039ff91..a39ea85e1d 100644 --- a/cypress/e2e/widgets.cy.js +++ b/cypress/e2e/widgets.cy.js @@ -29,17 +29,17 @@ describe('After user signed in', () => { it('first widget should be removed', () => { cy.get('.mk-widget-edit').click(); - cy.get('.data-cy-customize-container:first-child .data-cy-customize-container-remove._button').click(); - cy.get('.data-cy-customize-container').should('have.length', 2); + cy.get('[data-cy-customize-container]:first-child [data-cy-customize-container-remove]._button').click(); + cy.get('[data-cy-customize-container]').should('have.length', 2); }); function buildWidgetTest(widgetName) { it(`${widgetName} widget should get added`, () => { cy.get('.mk-widget-edit').click(); cy.get('.mk-widget-select select').select(widgetName, { force: true }); - cy.get('.data-cy-bg._modalBg.data-cy-transparent').click({ multiple: true, force: true }); + cy.get('[data-cy-bg]._modalBg[data-cy-transparent]').click({ multiple: true, force: true }); cy.get('.mk-widget-add').click({ force: true }); - cy.get(`.data-cy-mkw-${widgetName}`).should('exist'); + cy.get(`[data-cy-mkw-${widgetName}]`).should('exist'); }); } diff --git a/locales/de-DE.yml b/locales/de-DE.yml index eae9389acb..0716bcc4ad 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -122,6 +122,8 @@ unmarkAsSensitive: "Als nicht NSFW markieren" enterFileName: "Dateinamen eingeben" mute: "Stummschalten" unmute: "Stummschaltung aufheben" +renoteMute: "Renotes stummschalten" +renoteUnmute: "Renote-Stummschaltung aufheben" block: "Blockieren" unblock: "Blockierung aufheben" suspend: "Sperren" @@ -153,6 +155,7 @@ flagShowTimelineReplies: "Antworten in der Chronik anzeigen" flagShowTimelineRepliesDescription: "Ist diese Option aktiviert, so werden Antworten von Benutzern auf die Notizen anderer Benutzer in der Chronik angezeigt." autoAcceptFollowed: "Follow-Anfragen von Benutzern, denen du folgst, automatisch akzeptieren" addAccount: "Benutzerkonto hinzufügen" +reloadAccountsList: "Benutzerkontoliste aktualisieren" loginFailed: "Anmeldung fehlgeschlagen" showOnRemote: "Auf Ursprungsinstanz ansehen" general: "Allgemein" @@ -544,6 +547,10 @@ userSuspended: "Dieser Benutzer wurde gesperrt." userSilenced: "Dieser Benutzer wurde instanzweit stummgeschaltet." yourAccountSuspendedTitle: "Dieses Benutzerkonto ist gesperrt" yourAccountSuspendedDescription: "Dieses Benutzerkonto wurde gesperrt, da es gegen die Nutzungsbedingungen dieses Servers verstoßen hat. Trete mit dem Betreiber in Kontakt, falls du weitere Details erfahren möchtest. Bitte erstelle kein neues Benutzerkonto." +tokenRevoked: "Ungültiger Token" +tokenRevokedDescription: "Der Token ist abgelaufen. Bitte melde dich erneut an." +accountDeleted: "Benutzerkonto wurde gelöscht" +accountDeletedDescription: "Dieses Konto wurde gelöscht." menu: "Menü" divider: "Trenner" addItem: "Element hinzufügen" @@ -959,6 +966,18 @@ invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst ein emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht" postToTheChannel: "In Kanal senden" cannotBeChangedLater: "Kann später nicht mehr geändert werden." +reactionAcceptance: "Reaktionsannahme" +likeOnly: "Nur \"Gefällt mir\"" +likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen" +rolesAssignedToMe: "Mir zugewiesene Rollen" +resetPasswordConfirm: "Wirklich Passwort zurücksetzen?" +sensitiveWords: "Sensible Wörter" +sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden." +notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar." +license: "Lizenz" +unfavoriteConfirm: "Wirklich aus Favoriten entfernen?" +myClips: "Meine Clips" +drivecleaner: "Drive-Reiniger" _achievements: earnedAt: "Freigeschaltet am" _types: @@ -1106,7 +1125,7 @@ _achievements: title: "Beliebt" description: "Die Anzahl deiner Follower hat 100 überschritten" _followers300: - title: "Stellt euch bitte in einer Reihe auf" + title: "Eine geordnete Reihe, bitte!" description: "Die Anzahl deiner Follower hat 300 überschritten" _followers500: title: "Funkmast" @@ -1218,6 +1237,8 @@ _role: iconUrl: "Icon-URL" asBadge: "Als Abzeichen anzeigen" descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt." + displayOrder: "Position" + descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position." canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten." priority: "Priorität" @@ -1243,6 +1264,7 @@ _role: rateLimitFactor: "Versuchsanzahl" descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." canHideAds: "Kann Werbung ausblenden" + canSearchNotes: "Nutzung der Notizsuchfunktion" _condition: isLocal: "Lokaler Benutzer" isRemote: "Benutzer fremder Instanz" @@ -1844,3 +1866,9 @@ _deck: _dialog: charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}" charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}" +_disabledTimeline: + title: "Chronik deaktiviert" + description: "Mit deinen jetzigen Rollen ist diese Chronik nicht verfügbar." +_drivecleaner: + orderBySizeDesc: "Absteigende Dateigrößen" + orderByCreatedAtAsc: "Aufsteigendes Erstelldatum" diff --git a/locales/en-US.yml b/locales/en-US.yml index 53a30c741e..5efb7b7d1e 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -67,7 +67,7 @@ import: "Import" export: "Export" files: "Files" download: "Download" -driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted." +driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted." unfollowConfirm: "Are you sure you want to unfollow {name}?" exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." importRequested: "You've requested an import. This may take a while." @@ -122,6 +122,8 @@ unmarkAsSensitive: "Unmark as NSFW" enterFileName: "Enter filename" mute: "Mute" unmute: "Unmute" +renoteMute: "Mute Renotes" +renoteUnmute: "Unmute Renotes" block: "Block" unblock: "Unblock" suspend: "Suspend" @@ -153,6 +155,7 @@ flagShowTimelineReplies: "Show replies in timeline" flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on." autoAcceptFollowed: "Automatically approve follow requests from users you're following" addAccount: "Add account" +reloadAccountsList: "Reload account list" loginFailed: "Failed to sign in" showOnRemote: "View on remote instance" general: "General" @@ -527,7 +530,7 @@ nothing: "There's nothing to see here" installedDate: "Authorized at" lastUsedDate: "Last used at" state: "State" -sort: "Sort" +sort: "Sorting order" ascendingOrder: "Ascending" descendingOrder: "Descending" scratchpad: "Scratchpad" @@ -544,6 +547,10 @@ userSuspended: "This user has been suspended." userSilenced: "This user is being silenced." yourAccountSuspendedTitle: "This account is suspended" yourAccountSuspendedDescription: "This account has been suspended due to breaking the server's terms of services or similar. Contact the administrator if you would like to know a more detailed reason. Please do not create a new account." +tokenRevoked: "Invalid token" +tokenRevokedDescription: "This token has expired. Please log in again." +accountDeleted: "Account deleted" +accountDeletedDescription: "This account has been deleted." menu: "Menu" divider: "Divider" addItem: "Add Item" @@ -959,6 +966,18 @@ invitationRequiredToRegister: "This instance is invite-only. You must enter a va emailNotSupported: "This instance does not support sending emails" postToTheChannel: "Post to channel" cannotBeChangedLater: "This cannot be changed later." +reactionAcceptance: "Reaction Acceptance" +likeOnly: "Only likes" +likeOnlyForRemote: "Only likes for remote instances" +rolesAssignedToMe: "Roles assigned to me" +resetPasswordConfirm: "Really reset your password?" +sensitiveWords: "Sensitive words" +sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks." +notesSearchNotAvailable: "Note search is unavailable." +license: "License" +unfavoriteConfirm: "Really remove from favorites?" +myClips: "My clips" +drivecleaner: "Drive Cleaner" _achievements: earnedAt: "Unlocked at" _types: @@ -1218,6 +1237,8 @@ _role: iconUrl: "Icon URL" asBadge: "Show as badge" descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on." + displayOrder: "Position" + descriptionOfDisplayOrder: "The higher the number, the higher its UI position." canEditMembersByModerator: "Allow moderators to edit the list of members for this role" descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." priority: "Priority" @@ -1243,6 +1264,7 @@ _role: rateLimitFactor: "Rate limit" descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " canHideAds: "Can hide ads" + canSearchNotes: "Usage of note search" _condition: isLocal: "Local user" isRemote: "Remote user" @@ -1844,3 +1866,9 @@ _deck: _dialog: charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}." charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}." +_disabledTimeline: + title: "Timeline disabled" + description: "You cannot use this timeline under your current roles." +_drivecleaner: + orderBySizeDesc: "Descending Filesizes" + orderByCreatedAtAsc: "Ascending Dates" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 3ad2c21ff7..3e73e4c5ea 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -122,6 +122,8 @@ unmarkAsSensitive: "Desmarcar como sensible" enterFileName: "Ingrese el nombre del archivo" mute: "Silenciar" unmute: "Dejar de silenciar" +renoteMute: "Silenciar renota" +renoteUnmute: "Desilenciar renota" block: "Bloquear" unblock: "Dejar de bloquear" suspend: "Suspender" @@ -153,6 +155,7 @@ flagShowTimelineReplies: "Mostrar respuestas a las notas en la biografÃa" flagShowTimelineRepliesDescription: "Cuando se marca, la lÃnea de tiempo muestra respuestas a otras notas además de las notas del usuario" autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues" addAccount: "Agregar Cuenta" +reloadAccountsList: "Recargar lista de cuentas" loginFailed: "Error al iniciar sesión." showOnRemote: "Ver en una instancia remota" general: "General" @@ -506,6 +509,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir " serverLogs: "Registros del servidor" deleteAll: "Eliminar todos" showFixedPostForm: "Mostrar el formulario de las entradas encima de la lÃnea de tiempo" +showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronologÃa (Canales)" newNoteRecived: "Tienes una nota nueva" sounds: "Sonidos" sound: "Sonidos" @@ -543,6 +547,10 @@ userSuspended: "Este usuario ha sido suspendido." userSilenced: "Este usuario ha sido silenciado." yourAccountSuspendedTitle: "Esta cuenta ha sido suspendida" yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violaciones de los términos de servicio del servidor y otras razones. Para más información, póngase en contacto con el administrador. Por favor, no cree una nueva cuenta." +tokenRevoked: "Token inválido" +tokenRevokedDescription: "Este token expiró, vuelve a iniciar sesión." +accountDeleted: "Cuenta borrada" +accountDeletedDescription: "Esta cuenta ha sido borrada." menu: "Menú" divider: "Divisor" addItem: "Agregar elemento" @@ -955,6 +963,16 @@ exploreOtherServers: "Buscar otra instancia" letsLookAtTimeline: "Mirar la lÃnea de tiempo local" disableFederationWarn: "Esto desactivará la federación, pero las publicaciones segurán siendo públicas al menos que se configure diferente. Usualmente no necesitas usar esta configuración." invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido." +emailNotSupported: "Esta instancia no soporta el envÃo de correo electrónico" +postToTheChannel: "Publicar en el canal" +cannotBeChangedLater: "Esto no podrá ser cambiado después." +reactionAcceptance: "Aceptación de reacciones" +likeOnly: "Sólo 'me gusta'" +likeOnlyForRemote: "Sólo reacciones de instancias remotas" +rolesAssignedToMe: "Roles asignados a mÃ" +resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?" +sensitiveWords: "Palabras sensibles" +sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de lÃnea" _achievements: earnedAt: "Desbloqueado el" _types: @@ -1214,6 +1232,8 @@ _role: iconUrl: "URL del Ãcono" asBadge: "Mostrar como emblema" descriptionOfAsBadge: "Este Ãcono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo." + displayOrder: "Posición" + descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz." canEditMembersByModerator: "Permitir a los moderadores editar los miembros" descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo." priority: "Prioridad" @@ -1239,6 +1259,7 @@ _role: rateLimitFactor: "Limitador" descriptionOfRateLimitFactor: "LÃmites más bajos son menos restrictivos, más altos menos restrictivos" canHideAds: "Puede ocultar anuncios" + canSearchNotes: "Uso de la búsqueda de notas" _condition: isLocal: "Usuario local" isRemote: "Usuario remoto" @@ -1840,3 +1861,6 @@ _deck: _dialog: charactersExceeded: "¡Has excedido el lÃmite de caracteres! Actualmente {current} de {max}." charactersBelow: "¡Estás por debajo del lÃmite de caracteres! Actualmente {current} de {min}." +_disabledTimeline: + title: "LÃnea de tiempo deshabilitada" + description: "No puedes usar esta lÃnea de tiempo con tus roles actuales." diff --git a/locales/it-IT.yml b/locales/it-IT.yml index d5638aeb62..44499fa3dd 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -122,6 +122,8 @@ unmarkAsSensitive: "Segna come non sensibile" enterFileName: "Nome del file" mute: "Silenzia" unmute: "Riattiva l'audio" +renoteMute: "Silenzia i Rinota" +renoteUnmute: "Non silenziare i Rinota" block: "Blocca" unblock: "Sblocca" suspend: "Sospendi" @@ -153,6 +155,7 @@ flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline." flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le risposte alle altre note dell'utente oltre a quelle dell'utente stesso." autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui" addAccount: "Aggiungi profilo" +reloadAccountsList: "Ricarica l'elenco dei profili" loginFailed: "Accesso non riuscito" showOnRemote: "Leggi sull'istanza remota" general: "Generali" @@ -544,6 +547,10 @@ userSuspended: "L'utente è in sospensione" userSilenced: "L'utente è silenziat@." yourAccountSuspendedTitle: "Questo profilo è sospeso" yourAccountSuspendedDescription: "Questo profilo è stato sospeso a causa di una violazione del regolamento. Per informazioni, contattare l'amministrazione. Si prega di non creare un nuovo account." +tokenRevoked: "Il token non è valido" +tokenRevokedDescription: "Il token di accesso è scaduto. Per favore, accedi nuovamente." +accountDeleted: "Profilo eliminato" +accountDeletedDescription: "Questo profilo è stato eliminato." menu: "Menù" divider: "Linea di separazione" addItem: "Aggiungi elemento" @@ -959,6 +966,17 @@ invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi i emailNotSupported: "L'istanza non supporta l'invio di email" postToTheChannel: "Pubblica sul canale" cannotBeChangedLater: "Non sarà più modificabile" +reactionAcceptance: "Accettazione reazioni" +likeOnly: "Solo i Like" +likeOnlyForRemote: "Solo Like remoti" +rolesAssignedToMe: "I miei ruoli" +resetPasswordConfirm: "Vuoi reimpostare la password?" +sensitiveWords: "Parole sensibili" +sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga." +notesSearchNotAvailable: "Non è possibile cercare tra le Note." +license: "Licenza" +unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?" +myClips: "Le mie Clip" _achievements: earnedAt: "Data di conseguimento" _types: @@ -1218,6 +1236,8 @@ _role: iconUrl: "URL dell'icona" asBadge: "Mostra come badge" descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo." + displayOrder: "Ordine di visualizzazione" + descriptionOfDisplayOrder: "I valori più alti vengono visualizzati per primi" canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo" descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori." priority: "Priorità " @@ -1243,6 +1263,7 @@ _role: rateLimitFactor: "Limite del rapporto" descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." canHideAds: "Può nascondere i banner" + canSearchNotes: "Ricercare nelle Note" _condition: isLocal: "Profilo locale" isRemote: "Profilo remoto" @@ -1844,3 +1865,6 @@ _deck: _dialog: charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})" charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})" +_disabledTimeline: + title: "Timeline disabilitata" + description: "Il tuo ruolo non ha i permessi per accedere a questa timeline" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e441055cb0..2011ca3636 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -67,7 +67,7 @@ import: "インãƒãƒ¼ãƒˆ" export: "エクスãƒãƒ¼ãƒˆ" files: "ファイル" download: "ダウンãƒãƒ¼ãƒ‰" -driveFileDeleteConfirm: "ファイル「{name}ã€ã‚’削除ã—ã¾ã™ã‹ï¼Ÿã“ã®ãƒ•ァイルを添付ã—ãŸãƒŽãƒ¼ãƒˆã‚‚消ãˆã¾ã™ã€‚" +driveFileDeleteConfirm: "ファイル「{name}ã€ã‚’削除ã—ã¾ã™ã‹ï¼Ÿã“ã®ãƒ•ァイルを使用ã—ãŸå…¨ã¦ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„ã‹ã‚‰ã‚‚削除ã•れã¾ã™ã€‚" unfollowConfirm: "{name}ã®ãƒ•ã‚©ãƒãƒ¼ã‚’解除ã—ã¾ã™ã‹ï¼Ÿ" exportRequested: "エクスãƒãƒ¼ãƒˆã‚’リクエストã—ã¾ã—ãŸã€‚ã“れã«ã¯æ™‚é–“ãŒã‹ã‹ã‚‹å ´åˆãŒã‚りã¾ã™ã€‚エクスãƒãƒ¼ãƒˆãŒçµ‚ã‚ã‚‹ã¨ã€ã€Œãƒ‰ãƒ©ã‚¤ãƒ–ã€ã«è¿½åŠ ã•れã¾ã™ã€‚" importRequested: "インãƒãƒ¼ãƒˆã‚’リクエストã—ã¾ã—ãŸã€‚ã“れã«ã¯æ™‚é–“ãŒã‹ã‹ã‚‹å ´åˆãŒã‚りã¾ã™ã€‚" @@ -122,6 +122,8 @@ unmarkAsSensitive: "閲覧注æ„を解除ã™ã‚‹" enterFileName: "ファイルåを入力" mute: "ミュート" unmute: "ミュート解除" +renoteMute: "リノートをミュート" +renoteUnmute: "リノートã®ãƒŸãƒ¥ãƒ¼ãƒˆã‚’解除" block: "ブãƒãƒƒã‚¯" unblock: "ブãƒãƒƒã‚¯è§£é™¤" suspend: "å‡çµ" @@ -153,6 +155,7 @@ flagShowTimelineReplies: "タイムラインã«ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示㙠flagShowTimelineRepliesDescription: "オンã«ã™ã‚‹ã¨ã€ã‚¿ã‚¤ãƒ ラインã«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆä»¥å¤–ã«ã‚‚ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ä»–ã®ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã—ã¾ã™ã€‚" autoAcceptFollowed: "フォãƒãƒ¼ä¸ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒªã‚¯ã‚’自動承èª" addAccount: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’è¿½åŠ " +reloadAccountsList: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãƒªã‚¹ãƒˆã®æƒ…å ±ã‚’æ›´æ–°" loginFailed: "ãƒã‚°ã‚¤ãƒ³ã«å¤±æ•—ã—ã¾ã—ãŸ" showOnRemote: "リモートã§è¡¨ç¤º" general: "全般" @@ -544,6 +547,10 @@ userSuspended: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯å‡çµã•れã¦ã„ã¾ã™ã€‚" userSilenced: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã‚µã‚¤ãƒ¬ãƒ³ã‚¹ã•れã¦ã„ã¾ã™ã€‚" yourAccountSuspendedTitle: "アカウントãŒå‡çµã•れã¦ã„ã¾ã™" yourAccountSuspendedDescription: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ã€ã‚µãƒ¼ãƒãƒ¼ã®åˆ©ç”¨è¦ç´„ã«é•åã—ãŸãªã©ã®ç†ç”±ã«ã‚ˆã‚Šã€å‡çµã•れã¦ã„ã¾ã™ã€‚詳細ã«ã¤ã„ã¦ã¯ç®¡ç†è€…ã¾ã§ãŠå•ã„åˆã‚ã›ãã ã•ã„。新ã—ã„アカウントを作らãªã„ã§ãã ã•ã„。" +tokenRevoked: "トークンãŒç„¡åйã§ã™" +tokenRevokedDescription: "ãƒã‚°ã‚¤ãƒ³ãƒˆãƒ¼ã‚¯ãƒ³ãŒå¤±åйã—ã¦ã„ã¾ã™ã€‚ãƒã‚°ã‚¤ãƒ³ã—ç›´ã—ã¦ãã ã•ã„。" +accountDeleted: "アカウントã¯å‰Šé™¤ã•れã¦ã„ã¾ã™" +accountDeletedDescription: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯å‰Šé™¤ã•れã¦ã„ã¾ã™ã€‚" menu: "メニュー" divider: "分割線" addItem: "é …ç›®ã‚’è¿½åŠ " @@ -959,6 +966,21 @@ invitationRequiredToRegister: "ç¾åœ¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯æ‹›å¾…制ã§ã™ã€‚æ‹›å emailNotSupported: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã§ã¯ãƒ¡ãƒ¼ãƒ«é…ä¿¡ã¯ã‚µãƒãƒ¼ãƒˆã•れã¦ã„ã¾ã›ã‚“" postToTheChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã«æŠ•稿" cannotBeChangedLater: "後ã‹ã‚‰å¤‰æ›´ã§ãã¾ã›ã‚“。" +reactionAcceptance: "リアクションã®å—ã‘入れ" +likeOnly: "ã„ã„ãã®ã¿" +likeOnlyForRemote: "リモートã‹ã‚‰ã¯ã„ã„ãã®ã¿" +rolesAssignedToMe: "自分ã«å‰²ã‚Šå½“ã¦ã‚‰ã‚ŒãŸãƒãƒ¼ãƒ«" +resetPasswordConfirm: "パスワードリセットã—ã¾ã™ã‹ï¼Ÿ" +sensitiveWords: "センシティブワード" +sensitiveWordsDescription: "è¨å®šã—ãŸãƒ¯ãƒ¼ãƒ‰ãŒå«ã¾ã‚Œã‚‹ãƒŽãƒ¼ãƒˆã®å…¬é–‹ç¯„囲をホームã«ã—ã¾ã™ã€‚改行ã§åŒºåˆ‡ã£ã¦è¤‡æ•°è¨å®šã§ãã¾ã™ã€‚" +notesSearchNotAvailable: "ノート検索ã¯åˆ©ç”¨ã§ãã¾ã›ã‚“。" +license: "ライセンス" +unfavoriteConfirm: "ãŠæ°—ã«å…¥ã‚Šè§£é™¤ã—ã¾ã™ã‹ï¼Ÿ" +myClips: "自分ã®ã‚¯ãƒªãƒƒãƒ—" +drivecleaner: "ドライブクリーナー" +retryAllQueuesNow: "ã™ã¹ã¦ã®ã‚ューを今ã™ãå†è©¦è¡Œ" +retryAllQueuesConfirmTitle: "今ã™ãå†è©¦è¡Œã—ã¾ã™ã‹ï¼Ÿ" +retryAllQueuesConfirmText: "一時的ã«ã‚µãƒ¼ãƒãƒ¼ã®è² è·ãŒå¢—大ã™ã‚‹ã“ã¨ãŒã‚りã¾ã™ã€‚" _achievements: earnedAt: "ç²å¾—日時" @@ -1220,6 +1242,8 @@ _role: iconUrl: "アイコン画åƒã®URL" asBadge: "ãƒãƒƒã‚¸ã¨ã—ã¦è¡¨ç¤º" descriptionOfAsBadge: "オンã«ã™ã‚‹ã¨ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼åã®æ¨ªã«ãƒãƒ¼ãƒ«ã®ã‚¢ã‚¤ã‚³ãƒ³ãŒè¡¨ç¤ºã•れã¾ã™ã€‚" + displayOrder: "è¡¨ç¤ºé †" + descriptionOfDisplayOrder: "数値ãŒå¤§ãã„ã»ã©UI上ã§å…ˆé ã«è¡¨ç¤ºã•れã¾ã™ã€‚" canEditMembersByModerator: "モデレーターã®ãƒ¡ãƒ³ãƒãƒ¼ç·¨é›†ã‚’許å¯" descriptionOfCanEditMembersByModerator: "オンã«ã™ã‚‹ã¨ã€ç®¡ç†è€…ã«åŠ ãˆã¦ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ã‚‚ã“ã®ãƒãƒ¼ãƒ«ã¸ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’アサイン/アサイン解除ã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚オフã«ã™ã‚‹ã¨ç®¡ç†è€…ã®ã¿ãŒè¡Œãˆã¾ã™ã€‚" priority: "優先度" @@ -1245,6 +1269,7 @@ _role: rateLimitFactor: "レートリミット" descriptionOfRateLimitFactor: "å°ã•ã„ã»ã©åˆ¶é™ãŒç·©å’Œã•れã€å¤§ãã„ã»ã©åˆ¶é™ãŒå¼·åŒ–ã•れã¾ã™ã€‚" canHideAds: "広告ã®éžè¡¨ç¤º" + canSearchNotes: "ノート検索ã®åˆ©ç”¨å¯å¦" _condition: isLocal: "ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼" isRemote: "リモートユーザー" @@ -1897,3 +1922,11 @@ _deck: _dialog: charactersExceeded: "æœ€å¤§æ–‡å—æ•°ã‚’è¶…ãˆã¦ã„ã¾ã™ï¼ ç¾åœ¨ {current} / åˆ¶é™ {max}" charactersBelow: "æœ€å°æ–‡å—数を下回ã£ã¦ã„ã¾ã™ï¼ ç¾åœ¨ {current} / åˆ¶é™ {min}" + +_disabledTimeline: + title: "無効化ã•れãŸã‚¿ã‚¤ãƒ ライン" + description: "ç¾åœ¨ã®ãƒãƒ¼ãƒ«ã§ã¯ã€ã“ã®ã‚¿ã‚¤ãƒ ラインを使用ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。" + +_drivecleaner: + orderBySizeDesc: "サイズãŒå¤§ãã„é †" + orderByCreatedAtAsc: "è¿½åŠ æ—¥ãŒå¤ã„é †"
\ No newline at end of file diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 081457c2ff..bd9ae46d34 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -2,7 +2,7 @@ _lang_: "日本語 (関西å¼)" headlineMisskey: "ノートã§ã¤ãªãŒã‚‹ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯" introMisskey: "よã†ãŠè¶Šã—ï¼Misskeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã‚¤ã‚¯ãƒãƒ–ãƒã‚°ã‚µãƒ¼ãƒ“スやãん。\n「ノートã€ã‚’作ã£ã¦ã€ã„ã¾èµ·ã“ã£ã¨ã‚‹ã“ã¨ã‚’共有ã—ãŸã‚Šã€ã‚ã‚“ãŸã«ã¤ã„ã¦çš†ã«ç™ºä¿¡ã—よã†ðŸ“¡\nã€Œãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã€æ©Ÿèƒ½ã§ã€çš†ã®ãƒŽãƒ¼ãƒˆã«ç´ æ—©ãåå¿œã‚’è¿½åŠ ã—ãŸã‚Šã‚‚ã§ãã‚‹ã§âœŒ\nã»ãªæ–°ã—ã„世界を探検ã—よã‹ðŸš€" -poweredByMisskeyDescription: "{name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Misskey</b>を使ã£ãŸã‚µãƒ¼ãƒ“ス(Misskeyインスタンスã¨å‘¼ã°ã‚Œã‚‹ã‚„ã¤ã‚„)ã®ã²ã¨ã¤ã‚„ã§ã€‚" +poweredByMisskeyDescription: "{name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Misskey</b>ã®ã‚µãƒ¼ãƒãƒ¼ã®ã²ã¨ã¤ãªã‚“ã‚„ã§ã€‚" monthAndDay: "{month}月 {day}æ—¥" search: "探ã™" notifications: "通知" @@ -15,13 +15,13 @@ gotIt: "ã»ã„" cancel: "ã‚„ã‚ã¨ã" noThankYou: "ã‚„ã‚ã¨ã" enterUsername: "ユーザーåを入れã¦ã‚„" -renotedBy: "{user}ãŒRenote" -noNotes: "ノートã¯ã‚らã¸ã‚“" -noNotifications: "通知ã¯ã‚らã¸ã‚“" -instance: "インスタンス" +renotedBy: "{user}ãŒRenoteã—ãŸã§" +noNotes: "ノートãªã‚“ã¦ã‚らã¸ã‚“ã§" +noNotifications: "通知ãªã‚“ã¦ã‚らã¸ã‚“ã§" +instance: "サーãƒãƒ¼" settings: "è¨å®š" basicSettings: "基本è¨å®š" -otherSettings: "ãã®ä»–ã®è¨å®š" +otherSettings: "ã»ã‹ã®è¨å®š" openInWindow: "ウィンドウã§é–‹ãã§" profile: "プãƒãƒ•ィール" timeline: "タイムライン" @@ -55,7 +55,7 @@ searchUser: "ユーザーを検索" reply: "返事" loadMore: "ã¾ã ã¾ã ã‚ã‚‹ã§ï¼" showMore: "ã¾ã ã¾ã ã‚ã‚‹ã§ï¼" -showLess: "é–‰ã˜ã‚‹" +showLess: "ã•ã„ãªã‚‰" youGotNewFollower: "フォãƒãƒ¼ã•れãŸã§" receiveFollowRequest: "フォãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã•れãŸã§" followRequestAccepted: "フォãƒãƒ¼ãŒæ‰¿èªã•れãŸã§" @@ -84,12 +84,12 @@ error: "エラー" somethingHappened: "ãªã‚“ã‹ã‚¢ã‚«ãƒ³ã“ã¨ãŒèµ·ã“ã£ãŸã§" retry: "ã‚‚ã£ãºã‚“やる?" pageLoadError: "ページã®èªã¿è¾¼ã¿ã«å¤±æ•—ã—ã¦ã‚‚ã†ãŸã‚…" -pageLoadErrorDescription: "ã“ã‚Œã¯æ™®é€šã€ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‹ãƒ–ラウザã‚ャッシュãŒåŽŸå› ã‚„ã‹ã‚‰ã。ã‚ャッシュをクリアã™ã‚‹ã‹ã€ã‚‚ã†ã¡ã£ã¨ã ã‘å¾…ã£ã¦ãれã¸ã‚“ã‹ï¼Ÿ" +pageLoadErrorDescription: "ã“ã‚Œã¯æ™®é€šãªã‚‰ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‹ãƒ–ラウザã‚ãƒ£ãƒƒã‚·ãƒ¥ãŒæ‚ªã•ã—ã¦ã‚‹ã‚“よ。ã‚ャッシュをã»ã‹ã™ã‹ã€ã‚‚ã†ã¡ã‚‡ã£ã¨ã ã‘å¾…ã£ã¦ãれã¸ã‚“?" serverIsDead: "サーãƒãƒ¼ã‹ã‚‰ã®å¿œç”ãŒãªã„ã§ã€‚ã‚‚ã†ã¡ã‚‡ã„å¾…ã£ã¦ã‹ã‚‰è©¦ã—ã¦ã¿ã¦ãªã€‚" youShouldUpgradeClient: "ã“ã®ãƒšãƒ¼ã‚¸ã‚’表示ã™ã‚‹ã«ã¯ã€ãƒªãƒãƒ¼ãƒ‰ã—ã¦æ–°ã—ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã‚’使ã£ã¦ãªãƒ¼ã€‚" enterListName: "リストåを入れã¦ã‚„" privacy: "プライãƒã‚·ãƒ¼" -makeFollowManuallyApprove: "他人ã®ãƒ•ã‚©ãƒãƒ¼ã¯è¨±å¯ã—ã¦ã‹ã‚‰ã‚„ï¼" +makeFollowManuallyApprove: "ãˆãˆã£ã¦è¨€ã‚ãªãƒ•ã‚©ãƒãƒ¼ã§ãã¸ã‚“よã†ã«ã™ã‚‹" defaultNoteVisibility: "ã‚‚ã¨ã‹ã‚‰ã®å…¬é–‹ç¯„囲" follow: "フォãƒãƒ¼" followRequest: "フォãƒãƒ¼ã‚’é ¼ã‚€" @@ -113,7 +113,7 @@ sensitive: "ã¡ã‚‡ã£ã¨ã‚¢ã‚«ãƒ³ã‚„ã¤ã‚„ã§" add: "増やã™" reaction: "リアクション" reactions: "リアクション" -reactionSetting: "Reaction that will be displayed in Picker. " +reactionSetting: "ピッカーã«å‡ºã—ã¨ãリアクション" reactionSettingDescription2: "ドラッグã§ä¸¦ã³æ›¿ãˆã€ã‚¯ãƒªãƒƒã‚¯ã§å‰Šé™¤ã€ï¼‹ã‚’押ã—ã¦è¿½åŠ ã‚„ã§ã€‚" rememberNoteVisibility: "公開範囲覚ãˆã¨ã„ã¦" attachCancel: "ã®ã£ã‘ã‚‹ã®ã‚„ã‚ã‚‹" @@ -122,6 +122,8 @@ unmarkAsSensitive: "ãã“ã¾ã§ã‚¢ã‚«ãƒ³ã“ã¨ãªã„ã‚„ã‚" enterFileName: "ファイルåを入れã¦ã‚„" mute: "ミュート" unmute: "ミュートやã‚ãŸã‚‹" +renoteMute: "リノートã¯è¦‹ã„ã²ã‚“" +renoteUnmute: "リノートもやã£ã±è¦‹ã‚‹ã‚" block: "ブãƒãƒƒã‚¯" unblock: "ブãƒãƒƒã‚¯ã‚„ã‚ãŸã‚‹" suspend: "å‡çµ" @@ -145,20 +147,21 @@ addEmoji: "絵文å—ã‚’è¿½åŠ " settingGuide: "ãˆãˆæ„Ÿã˜ã®è¨å®š" cacheRemoteFiles: "リモートã®ãƒ•ァイルをã‚ャッシュã™ã‚‹" cacheRemoteFilesDescription: "ã“ã®è¨å®šã‚’切ã£ã¨ãã¨ã€ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ァイルをã‚ャッシュã›ãšç›´ãƒªãƒ³ã‚¯ã™ã‚‹ã‚ˆã†ã«ãªã‚‹ã§ã€‚サーãƒãƒ¼ã®å®¹é‡ã¯ç¯€ç´„ã§ãã‚‹ã‘ã©ã€ã‚µãƒ ãƒã‚¤ãƒ«ãŒä½œã‚‰ã‚Œã‚“ããªã‚‹ã‹ã‚‰é€šä¿¡é‡ãŒå¢—ãˆã‚‹ã§ã€‚" -flagAsBot: "Botã‚„ã§" -flagAsBotDescription: "ã‚‚ã—ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒãƒ—ãƒã‚°ãƒ©ãƒ ã«ã‚ˆã£ã¦é‹ç”¨ã•れるんやã£ãŸã‚‰ã€ã“ã®ãƒ•ラグをオンã«ã—ã¦ãŸã®ã‚€ã§ã€‚オンã«ã™ã‚‹ã¨ã€å応ã®é€£éŽ–ã‚’é˜²ããŸã‚ã®ãƒ•ラグã¨ã—ã¦ä»–ã®é–‹ç™ºè€…ã«å½¹ç«‹ã£ãŸã‚Šã€Misskeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã‚“ã«ãªã‚‹ã‚“ã‚„ã§ã€‚" +flagAsBot: "Botã«ã™ã‚‹ã§" +flagAsBotDescription: "ã‚‚ã—ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’プãƒã‚°ãƒ©ãƒ 使ã†ã¦é‹ç”¨ã™ã‚‹ã‚“ã‚„ã£ãŸã‚‰ã€ã“ã®ãƒ•ラグをオンã«ã—ã¦ã‚„。オンã«ã™ã‚Œã°ã€å応ãŒãƒãƒ¼ãƒƒã¦é€£éŽ–ã™ã‚‹ã®ã‚’é¿ã‘ã‚‹ãŸã‚ã«é–‹ç™ºè€…ãŒä½¿ã†ãŸã‚Šã€Misskeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã‚“ã«ãªã‚‹ã‹ã‚‰ãªã€‚" flagAsCat: "Catã‚„ã§" flagAsCatDescription: "ワレã€çŒ«ã¡ã‚ƒã‚“ãªã‚‰ã“ã®ãƒ•ラグをã¤ã‘ã¦ã¿ï¼Ÿ" flagShowTimelineReplies: "タイムラインã«ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã™ã‚‹ã§" flagShowTimelineRepliesDescription: "オンã«ã—ãŸã‚‰ã€ã‚¿ã‚¤ãƒ ラインã«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆã®ä»–ã«ã‚‚ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ä»–ã®ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã™ã‚‹ã§ã€‚" autoAcceptFollowed: "フォãƒãƒ¼ã—ã¨ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’勿‰‹ã«è¨±å¯ã—ã¨ã" addAccount: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’è¿½åŠ " +reloadAccountsList: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãƒªã‚¹ãƒˆã®æƒ…å ±ã‚’æ›´æ–°" loginFailed: "ãƒã‚°ã‚¤ãƒ³ã«å¤±æ•—ã—ã¦ã‚‚ã†ãŸâ€¦" showOnRemote: "リモートã§è¦‹ã‚‹" general: "全般" wallpaper: "å£ç´™" setWallpaper: "å£ç´™ã‚’è¨å®š" -removeWallpaper: "å£ç´™ã‚’削除" +removeWallpaper: "å£ç´™ã»ã‹ã™" searchWith: "検索: {q}" youHaveNoLists: "リストãŒã‚らã¸ã‚“ã§ï¼Ÿ" followConfirm: "{name}をフォãƒãƒ¼ã—ã¦ãˆãˆã‹ï¼Ÿ" @@ -169,7 +172,7 @@ selectUser: "ユーザーをé¸ã¶" recipient: "宛先" annotation: "注釈" federation: "連åˆ" -instances: "インスタンス" +instances: "サーãƒãƒ¼" registeredAt: "åˆè¦³æ¸¬" latestRequestReceivedAt: "ã¡ã‚‡ã£ã¨å‰ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆå—ä¿¡" latestStatus: "ã¡ã‚‡ã£ã¨å‰ã®ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹" @@ -178,7 +181,7 @@ charts: "ãƒãƒ£ãƒ¼ãƒˆ" perHour: "1時間ã”ã¨" perDay: "1æ—¥ã”ã¨" stopActivityDelivery: "アクティビティã®é…é€ã‚’ã‚„ã‚ã‚‹" -blockThisInstance: "ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’ブãƒãƒƒã‚¯" +blockThisInstance: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‚’ブãƒãƒƒã‚¯ã™ã‚“ã§" operations: "æ“作" software: "ソフトウェア" version: "ãƒãƒ¼ã‚¸ãƒ§ãƒ³" @@ -189,28 +192,28 @@ jobQueue: "ジョブã‚ュー" cpuAndMemory: "CPUã¨ãƒ¡ãƒ¢ãƒª" network: "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯" disk: "ディスク" -instanceInfo: "ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹æƒ…å ±" +instanceInfo: "サーãƒãƒ¼æƒ…å ±" statistics: "統計" clearQueue: "ã‚ューã«ã•ã„ãªã‚‰" clearQueueConfirmTitle: "ã‚ューをクリアã—ã¾ã£ã‹ï¼Ÿ" -clearQueueConfirmText: "未é…é”ã®æŠ•ç¨¿ã¯é…é€ã•れãªããªã‚‹ã§ã€‚通常ã“ã®æ“作を行ã†å¿…è¦ã¯ã‚らã¸ã‚“や。" +clearQueueConfirmText: "未é…é”ã®æŠ•ç¨¿ã¯é…é€ã•れãªããªã‚‹ã§ã€‚ãµã¤ã†ã“ã®æ“作を行ã†å¿…è¦ã¯ç„¡ã„ã‚“ã‚„ã‘ã©ãªã€‚" clearCachedFiles: "ã‚ャッシュã«ã•ã„ãªã‚‰" clearCachedFilesConfirm: "ã‚ャッシュã•れã¨ã‚‹ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ァイルをã¿ã‚“ãªã»ã‹ã—ã¦ãˆãˆã‹ï¼Ÿ" -blockedInstances: "インスタンスブãƒãƒƒã‚¯" -blockedInstancesDescription: "ブãƒãƒƒã‚¯ã—ãŸã„インスタンスã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¦ãªã€‚ブãƒãƒƒã‚¯ã•れã¦ã‚‚ã†ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¨ã¯ã‚‚ã†é‡‘輪際やりå–りã§ãã²ã‚“ããªã‚‹ã§ã€‚" +blockedInstances: "ブãƒãƒƒã‚¯ã—ãŸã‚µãƒ¼ãƒãƒ¼" +blockedInstancesDescription: "ブãƒãƒƒã‚¯ã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¦ãªã€‚ブãƒãƒƒã‚¯ã•れã¦ã‚‚ã†ãŸã‚µãƒ¼ãƒãƒ¼ã¨ã¯ã‚‚ã†é‡‘輪際やりå–りã§ãã²ã‚“ããªã‚‹ã§ã€‚ã¤ã„ã§ã«ãã®ã‚µãƒ–ドメインもブãƒãƒƒã‚¯ã™ã‚‹ã§ã€‚" muteAndBlock: "ミュートã¨ãƒ–ãƒãƒƒã‚¯" mutedUsers: "ミュートã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" blockedUsers: "ブãƒãƒƒã‚¯ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" -noUsers: "ユーザーã¯ãŠã‚‰ã¸ã‚“" +noUsers: "ユーザーã¯ãŠã‚‰ã‚“" editProfile: "プãƒãƒ•ィールをã„ã˜ã‚‹" noteDeleteConfirm: "ã“ã®ãƒŽãƒ¼ãƒˆã‚’削除ã—ã¾ã£ã‹ï¼Ÿ" pinLimitExceeded: "ã“れ以上ピン留ã‚ã§ãã²ã‚“" -intro: "Misskeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ã¦ã‚“ï¼ç®¡ç†è€…アカウントを作ã£ã¦ã‚„。" +intro: "Misskeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ãŸã§ï¼ç®¡ç†è€…アカウントを作ã£ã¦ã‚„。" done: "ã§ã‘ãŸ" processing: "処ç†ã—ã¨ã‚‹" preview: "プレビュー" default: "デフォルト" -defaultValueIs: "デフォルト" +defaultValueIs: "デフォルト: {value}" noCustomEmojis: "絵文å—ã¯ã‚らã¸ã‚“" noJobs: "ジョブã¯ã‚らã¸ã‚“" federating: "連åˆã—ã¨ã‚‹" @@ -220,17 +223,17 @@ all: "ã¿ã‚“ãª" subscribing: "è³¼èªã—ã¨ã‚‹" publishing: "é…ä¿¡ã—ã¨ã‚‹" notResponding: "応ç”ã—ã¦ã¸ã‚“ã§" -instanceFollowing: "インスタンスã®ãƒ•ã‚©ãƒãƒ¼" -instanceFollowers: "インスタンスã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼\n" -instanceUsers: "インスタンスã®ãƒ¦ãƒ¼ã‚¶ãƒ¼" +instanceFollowing: "サーãƒãƒ¼ã®ãƒ•ã‚©ãƒãƒ¼" +instanceFollowers: "サーãƒãƒ¼ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼\n" +instanceUsers: "サーãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼" changePassword: "パスワード変ãˆã‚‹" security: "ã‚»ã‚ュリティ" -retypedNotMatch: "ãã‚„ãªã„ãん。" +retypedNotMatch: "入れãŸã‚„ã¤åŒã˜ã«ãªã£ã¦ãªã„ã§ã€‚" currentPassword: "今ã®ãƒ‘スワード" -newPassword: "今度ã®ãƒ‘スワード" +newPassword: "次ã®ãƒ‘スワード" newPasswordRetype: "今度ã®ãƒ‘スワード(ã‚‚ã£ãºã‚“入れã¦)" attachFile: "ファイルã®ã£ã‘ã‚‹" -more: "ä»–ã®ã‚„ã¤ï¼" +more: "ä»–ã®ã‚“" featured: "ãƒã‚¤ãƒ©ã‚¤ãƒˆ" usernameOrUserId: "ユーザーåã‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ID" noSuchUser: "ユーザーãŒè¦‹ã¤ã‹ã‚‰ã¸ã‚“ã§" @@ -238,15 +241,15 @@ lookup: "見ã¦ãã¦" announcements: "ãŠçŸ¥ã‚‰ã›" imageUrl: "ç”»åƒURL" remove: "ã»ã‹ã™" -removed: "削除ã—ãŸã§ï¼" +removed: "ã»ã‹ã—ãŸã§ï¼" removeAreYouSure: "「{x}ã€ã¯ã»ã‹ã—ã¦ãˆãˆã‹ï¼Ÿ" deleteAreYouSure: "「{x}ã€ã¯ã»ã‹ã—ã¦ãˆãˆã‹ï¼Ÿ" resetAreYouSure: "リセットã—ã¦ãˆãˆã‚“?" saved: "ä¿å˜ã—ãŸã§ï¼" messaging: "ãƒãƒ£ãƒƒãƒˆ" upload: "アップãƒãƒ¼ãƒ‰" -keepOriginalUploading: "オリジナル画åƒã‚’ä¿æŒã™ã‚‹ã‚" -keepOriginalUploadingDescription: "ç”»åƒã‚’上ã’ã‚‹ã¨ãã«ã‚ªãƒªã‚¸ãƒŠãƒ«ç‰ˆã‚’ä¿æŒã™ã‚‹ã§ã€‚オフã«ã—ãŸã‚‰ä¸Šã’ãŸã¨ãã«ãƒ–ラウザã§Web公開用ã®ç”»åƒã‚’生æˆã™ã‚‹ã§ã€‚ " +keepOriginalUploading: "オリジナル画åƒã®ã¾ã‚“ã¾" +keepOriginalUploadingDescription: "ç”»åƒã‚’上ã’ã‚‹ã¨ãã«ã‚ªãƒªã‚¸ãƒŠãƒ«ç‰ˆã®ã¾ã‚“ã¾ã«ã™ã‚‹ã§ã€‚オフã«ã—ãŸã‚‰ã€ä¸Šã’ãŸã¨ãã«ãƒ–ラウザã§Web公開用ã®ç”»åƒã‚’生æˆã™ã‚‹ã§ã€‚ " fromDrive: "ドライブã‹ã‚‰" fromUrl: "URLã‹ã‚‰" uploadFromUrl: "URLアップãƒãƒ¼ãƒ‰" @@ -272,8 +275,8 @@ yearsOld: "{age}æ³" registeredDate: "å§‹ã‚ãŸæ—¥" location: "å ´æ‰€" theme: "テーマ" -themeForLightMode: "ライトモードã§ã¯ã“ã®ãƒ†ãƒ¼ãƒžã¤ã“ã¦" -themeForDarkMode: "ダークモードã§ã¯ã“ã®ãƒ†ãƒ¼ãƒžã¤ã“ã¦" +themeForLightMode: "ライトモードã§ã¯ã“ã®ãƒ†ãƒ¼ãƒžä½¿ã†ã¦" +themeForDarkMode: "ダークモードã§ã¯ã“ã®ãƒ†ãƒ¼ãƒžä½¿ã†ã¦" light: "ライト" dark: "ダーク" lightThemes: "デイゲーム" @@ -289,13 +292,13 @@ renameFile: "ファイルåã‚’ã„らã†" folderName: "フォルダーå" createFolder: "フォルダー作る" renameFolder: "フォルダーåを変ãˆã‚‹" -deleteFolder: "フォルダーを消ã—ã¦ã¾ã†" +deleteFolder: "フォルダーをã»ã‹ã™" addFile: "ãƒ•ã‚¡ã‚¤ãƒ«ã‚’è¿½åŠ " emptyDrive: "ドライブã«ã¯ãªã‚“も残ã£ã¨ã‚‰ã‚“" -emptyFolder: "ãµã‰ã‚ã ーã«ã¯ãªã‚“も残ã£ã¨ã‚‰ã‚“" +emptyFolder: "ã“ã®ãƒ•ォルダーã¯ç©ºã‚„" unableToDelete: "消ãã†ãŠã‚‚ã£ã¦ã‚“ã‘ã©ãªã€ã‚ã‹ã‚“ã‹ã£ãŸã‚" inputNewFileName: "今度ã®ãƒ•ァイルåã¯ä½•ã«ã™ã‚‹ã‚“?" -inputNewDescription: "æ–°ã—ã„ã‚ャプションを入力ã—ã¾ã—ょ" +inputNewDescription: "æ–°ã—ã„ã‚ャプションを入れã¦ã‚„" inputNewFolderName: "今度ã®ãƒ•ォルダåã¯ä½•ã«ã™ã‚‹ã‚“?" circularReferenceFolder: "移動先ã®ãƒ•ォルダーã¯ã€ç§»å‹•ã™ã‚‹ãƒ•ォルダーã®ã‚µãƒ–フォルダーや。" hasChildFilesOrFolders: "ã“ã®ãƒ•ォルダã€ã¾ã ãªã‚“ã‹å…¥ã£ã¨ã‚‹ã‹ã‚‰æ¶ˆã•れã¸ã‚“" @@ -303,8 +306,8 @@ copyUrl: "URLをコピー" rename: "åå‰ã‚’変ãˆã‚‹ã§" avatar: "アイコン" banner: "ãƒãƒŠãƒ¼" -nsfw: "閲覧注æ„" -whenServerDisconnected: "サーãƒãƒ¼ã¨ã®æŽ¥ç¶šãŒåˆ‡ã‚ŒãŸã¨ã" +nsfw: "è¦‹ã‚‹ã‚“ã¯æ°—ã„ã¤ã‘ã¦ãª" +whenServerDisconnected: "サーãƒãƒ¼ã¨ã®æŽ¥ç¶šãŒå¤±ããªã£ã¦ã—ã‚‚ã†ãŸã¨ã" disconnectedFromServer: "サーãƒãƒ¼ãŒæ©Ÿå«Œæ‚ªã„ãã‚“" reload: "リãƒãƒ¼ãƒ‰" doNothing: "何もã›ã‚“ã¨ã" @@ -314,10 +317,10 @@ unwatch: "ウォッãƒã‚„ã‚ã‚‹" accept: "ãˆãˆã§" reject: "ã‚ã‹ã‚“" normal: "ãˆãˆæ„Ÿã˜" -instanceName: "インスタンスå" -instanceDescription: "インスタンスã®ç´¹ä»‹" -maintainerName: "管ç†è€…ã®åå‰" -maintainerEmail: "管ç†è€…ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹" +instanceName: "サーãƒãƒ¼å" +instanceDescription: "サーãƒãƒ¼ã®ç´¹ä»‹" +maintainerName: "管ç†è€…ã¯ã‚“ã®åå‰" +maintainerEmail: "管ç†è€…ã¯ã‚“ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹" tosUrl: "利用è¦ç´„ã®URL" thisYear: "今年" thisMonth: "今月" @@ -329,23 +332,23 @@ pages: "ページ" integration: "連æº" connectService: "ã¤ãªã’ã‚‹ã§" disconnectService: "切るã§" -enableLocalTimeline: "ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインを使ãˆã‚‹ã‚ˆã†ã«ã™ã‚‹" -enableGlobalTimeline: "ã‚°ãƒãƒ¼ãƒãƒ«ã‚¿ã‚¤ãƒ ラインを使ãˆã‚‹ã‚ˆã†ã«ã™ã‚‹" +enableLocalTimeline: "ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインを使ãˆã‚‹ã‚ˆã†ã«ã™ã‚‹ã‚" +enableGlobalTimeline: "ã‚°ãƒãƒ¼ãƒãƒ«ã‚¿ã‚¤ãƒ ラインを使ãˆã‚‹ã‚ˆã†ã«ã™ã‚‹ã‚" disablingTimelinesInfo: "ã“ã“らã¸ã‚“ã®ã‚¿ã‚¤ãƒ ラインを使ãˆã‚“よã†ã«ã—ã¦ã—ã‚‚ã¦ã‚‚ã€ç®¡ç†è€…ã¨ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ã¯ä½¿ãˆã‚‹ã¾ã¾ã«ãªã£ã¦ã‚‹ã§ã€ãã†ã‚„ãªã‹ã£ãŸã‚‰ä¸ä¾¿ã‚„ã‹ã‚‰ãªã€‚" registration: "登録" enableRegistration: "一見ã•ã‚“ã§ã‚‚誰ã§ã‚‚ã„らã£ã—ゃ~ã„" invite: "æ¥ã¦ã‚„" -driveCapacityPerLocalAccount: "ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" -driveCapacityPerRemoteAccount: "リモートユーザーã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" +driveCapacityPerLocalAccount: "ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã‚“ã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" +driveCapacityPerRemoteAccount: "リモートユーザーã¯ã‚“ã²ã¨ã‚Šã‚ãŸã‚Šã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡" inMb: "メガãƒã‚¤ãƒˆå˜ä½" iconUrl: "アイコン画åƒã®URL" bannerUrl: "ãƒãƒŠãƒ¼ç”»åƒã®URL" backgroundImageUrl: "背景画åƒã®URL" basicInfo: "åŸºæœ¬æƒ…å ±" pinnedUsers: "ピン留ã‚ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" -pinnedUsersDescription: "「ã¿ã¤ã‘ã‚‹ã€ãƒšãƒ¼ã‚¸ã¨ã‹ã«ãƒ”ン留ã‚ã—ãŸã„ユーザーをã“ã“ã«æ›¸ã‘ã°ãˆãˆã‚“ã‚„ã§ã€‚他ん人ã¨ã®åå‰ã¯æ”¹è¡Œã§åŒºåˆ‡ã‚Œã°ãˆãˆã‚“ã‚„ã§ã€‚" +pinnedUsersDescription: "「ã¿ã¤ã‘ã‚‹ã€ãƒšãƒ¼ã‚¸ã¨ã‹ã«ãƒ”ン留ã‚ã—ãŸã„ユーザーをã“ã“ã«æ›¸ã‘ã°ãˆãˆã‚“ã‚„ã§ã€‚ãƒ¦ãƒ¼ã‚¶ãƒ¼æ¯Žã«æ”¹è¡Œã—ã¦ã‚„。" pinnedPages: "ピン留ã‚ページ" -pinnedPagesDescription: "インスタンスã®ã„ã£ã¡ã‚ƒã‚“上ã«ãƒ”ン留ã‚ã—ãŸã„ページã®ãƒ‘スを改行ã§åŒºåˆ‡ã£ã¦è¨˜è¿°ã—ã¦ãª" +pinnedPagesDescription: "サーãƒãƒ¼ã®ã„ã£ã¡ã‚ƒã‚“上ã«ãƒ”ン留ã‚ã—ãŸã„ページã®ãƒ‘スを改行ã§åŒºåˆ‡ã£ã¦è¨˜è¿°ã—ã¦ãª" pinnedClipId: "ピン留ã‚ã™ã‚‹ã‚¯ãƒªãƒƒãƒ—ã®ID" pinnedNotes: "ピン留ã‚ã•れã¨ã‚‹ãƒŽãƒ¼ãƒˆ" hcaptcha: "hCaptcha(ã‚ャプãƒãƒ£ï¼‰" @@ -370,7 +373,7 @@ antennaExcludeKeywords: "除外ã‚ーワード" antennaKeywordsDescription: "スペースã§åŒºåˆ‡ã£ãŸã‚‹ã¨AND指定ã§ã€æ”¹è¡Œã§åŒºåˆ‡ã£ãŸã‚‹ã¨OR指定や" notifyAntenna: "æ–°ã—ã„ノートを通知ã™ã‚“ã§" withFileAntenna: "ãªã‚“ã‹æ·»ä»˜ã•れãŸãƒŽãƒ¼ãƒˆã ã‘" -enableServiceworker: "ServiceWorkerã‚’ã¤ã“ã¦" +enableServiceworker: "ブラウザã«ãƒ—ッシュ通知ãŒè¡Œãよã†ã«ã™ã‚‹" antennaUsersDescription: "ユーザーåを改行ã§åŒºåˆ‡ã£ãŸã£ã¦ãª" caseSensitive: "大文å—ã¨å°æ–‡å—ã¯åˆ¥ã‚‚ã‚“ã‚„" withReplies: "返信も入れãŸã£ã¦" @@ -395,23 +398,23 @@ administrator: "管ç†è€…" token: "トークン" 2fa: "二è¦ç´ èªè¨¼" totp: "èªè¨¼ã‚¢ãƒ—リ" -totpDescription: "èªè¨¼ã‚¢ãƒ—リ使ã¦ãƒ¯ãƒ³ã‚¿ã‚¤ãƒ パスワードを入れる" +totpDescription: "èªè¨¼ã‚¢ãƒ—リ使ã†ã¦ãƒ¯ãƒ³ã‚¿ã‚¤ãƒ パスワードを入れる" moderator: "モデレーター" moderation: "モデレーション" nUsersMentioned: "{n}äººãŒæŠ•ç¨¿" securityKeyAndPasskey: "ã‚»ã‚ュリティã‚ー・パスã‚ー" securityKey: "ã‚»ã‚ュリティã‚ー" lastUsed: "最後ã«ã¤ã“ã†ãŸæ—¥" -lastUsedAt: "最後ã«ä½¿ãŸã‚“: {t}" +lastUsedAt: "最後ã«ä½¿ã†ãŸã‚“ã¯: {t}" unregister: "登録やã‚ã‚‹" passwordLessLogin: "パスワード無ãã¦ã‚‚ãƒã‚°ã‚¤ãƒ³ã§ãるよã†ã«ã™ã‚‹" -passwordLessLoginDescription: "パスワードやãªãã¦ã€ã‚»ã‚ュリティã‚ーã¨ã‹ãƒ‘スã‚ーã ã‘ã§ãƒã‚°ã‚¤ãƒ³ã™ã‚‹ã‚" +passwordLessLoginDescription: "パスワードãªã‚“ã‹ã„らんã€ã‚»ã‚ュリティã‚ーã¨ã‹ãƒ‘スã‚ーã ã‘ã§ãƒã‚°ã‚¤ãƒ³ã™ã‚‹ã‚" resetPassword: "パスワードをリセット" newPasswordIs: "今度ã®ãƒ‘スワードã¯ã€Œ{password}ã€ã‚„" -reduceUiAnimation: "UIã®å‹•ãやアニメーションを減らã™" +reduceUiAnimation: "UIã®å‹•ãやアニメーションを少ãªã™ã‚‹" share: "ã‚ã‘ã‚ã‘" notFound: "見ã¤ã‹ã‚‰ã¸ã‚“ã" -notFoundDescription: "指定ã•れãŸURLã«è©²å½“ã™ã‚‹ãƒšãƒ¼ã‚¸ã¯ã‚らã¸ã‚“ã‚„ã£ãŸã€‚" +notFoundDescription: "言ã‚れãŸURLã«ã¯ã¾ã‚‹ãƒšãƒ¼ã‚¸ã¯ãªã‹ã£ãŸã§ã€‚" uploadFolder: "ã¨ã‚Šã‚ãˆãšã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã—ãŸã‚„ã¤ç½®ã„ã¨ã所" cacheClear: "ã‚ャッシュをã»ã‹ã™" markAsReadAllNotifications: "通知ã¯ã‚‚ã†å…¨ã¦èªã‚“ã ã‚ã£" @@ -419,37 +422,37 @@ markAsReadAllUnreadNotes: "投稿ã¯å…¨ã¦èªã‚“ã ã‚ã£" markAsReadAllTalkMessages: "ãƒãƒ£ãƒƒãƒˆã¯ã‚‚ã†ãœã‚“ã¶èªã‚“ã ã‚ã£" help: "ヘルプ" inputMessageHere: "ã“ã“ã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸æ›¸ã„ã¦ã‚„" -close: "é–‰ã˜ã‚‹" +close: "ã•ã„ãªã‚‰" invites: "æ¥ã¦ã‚„" -members: "メンãƒãƒ¼" +members: "メンãƒãƒ¼ã¯ã‚“" transfer: "è²æ¸¡" title: "タイトル" text: "テã‚スト" enable: "有効ã«ã™ã‚‹ã§" next: "次" retype: "ã‚‚ã£ã‹ã„入力" -noteOf: "{user}ã®ãƒŽãƒ¼ãƒˆ" +noteOf: "{user}ã¯ã‚“ã®ãƒŽãƒ¼ãƒˆ" quoteAttached: "引用付ã„ã¨ã‚‹ã§" quoteQuestion: "引用ã¨ã—ã¦æ·»ä»˜ã—ã¦ã‚‚ãˆãˆã‹ï¼Ÿ" noMessagesYet: "ã¾ã ãƒãƒ£ãƒƒãƒˆã¯ã‚らã¸ã‚“ã§" newMessageExists: "æ–°ã—ã„メッセージãŒããŸã§" -onlyOneFileCanBeAttached: "ã™ã¾ã‚“ã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã«æ·»ä»˜ã§ãるファイルã¯ã²ã¨ã¤ã ã‘ãªã‚“や。" +onlyOneFileCanBeAttached: "ã”ã‚ã‚“ãªã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã«æ·»ä»˜ã§ãるファイルã¯ã²ã¨ã¤ã ã‘ãªã‚“よ。" signinRequired: "ãƒã‚°ã‚¤ãƒ³ã—ã¦ãれã¸ã‚“?" invitations: "æ¥ã¦ã‚„" invitationCode: "招待コード" checking: "確èªã—ã¨ã‚‹ã§" -available: "利用ã§ãã‚‹\n" +available: "使ãˆã‚‹ã§" unavailable: "利用ã§ãã‚“" usernameInvalidFormat: "a~zã€A~Zã€0~9ã€_ãŒä½¿ãˆã‚‹ã§" tooShort: "çŸã™ãŽã‚„ã‚ï¼" tooLong: "é•·ã™ãŽã‚„ã‚ï¼" weakPassword: "ã¸ã¼ã„パスワード" -normalPassword: "普通ã®ãƒ‘スワード" +normalPassword: "ã¼ã¡ã¼ã¡ã®ãƒ‘スワード" strongPassword: "ãˆãˆæ„Ÿã˜ã®ãƒ‘スワード" passwordMatched: "よã—ï¼ä¸€è‡´ã‚„ï¼" passwordNotMatched: "一致ã—ã¨ã‚‰ã‚“ã§ï¼Ÿ" signinWith: "{x}ã§ãƒã‚°ã‚¤ãƒ³" -signinFailed: "ãƒã‚°ã‚¤ãƒ³ã§ãã‚“ã‹ã£ãŸã§ã€‚ã‚‚ã£ã‹ã„ユーザーåã¨ãƒ‘スワードを確èªã—ã¦ã¿ã¦ãªã€‚" +signinFailed: "ãƒã‚°ã‚¤ãƒ³ã§ãã‚“ã‹ã£ãŸã§ã€‚ã‚‚ã£ã‹ã„ユーザーåã¨ãƒ‘スワードを確èªã—ã¦ã¿ã¦ã‚„。" or: "ãれã‹" language: "言語" uiLanguage: "UIã®è¡¨ç¤ºè¨€èªž" @@ -458,7 +461,7 @@ emojiStyle: "絵文å—ã®ã‚¹ã‚¿ã‚¤ãƒ«" native: "ãƒã‚¤ãƒ†ã‚£ãƒ–" disableDrawer: "メニューをドãƒãƒ¯ãƒ¼ã§è¡¨ç¤ºã›ã‡ã¸ã‚“" showNoteActionsOnlyHover: "ãƒŽãƒ¼ãƒˆã®æ“作部をホãƒãƒ¼æ™‚ã®ã¿è¡¨ç¤ºã™ã‚‹ã§" -noHistory: "å±¥æ´ã¯ã‚らã¸ã‚“ãã‡ã€‚" +noHistory: "å±¥æ´ã¯ãªã„ã‚。" signinHistory: "ãƒã‚°ã‚¤ãƒ³å±¥æ´" enableAdvancedMfm: "ã‚„ã‚„ã“ã—ã„MFMã‚‚ã‚りã«ã™ã‚‹" enableAnimatedMfm: "å‹•ããŒã‚„ã‹ã¾ã—ã„MFMも許ã—ãŸã‚‹" @@ -466,12 +469,12 @@ doing: "ã‚„ã£ã¨ã‚‹ãŒãª" category: "カテゴリ" tags: "ã‚¿ã‚°" docSource: "ã“ã®ãƒ‰ã‚ュメントã®ã‚½ãƒ¼ã‚¹" -createAccount: "アカウントを作æˆ" -existingAccount: "æ—¢å˜ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆ" -regenerate: "å†ç”Ÿæˆ" -fontSize: "フォントサイズ" +createAccount: "アカウントを作るã§" +existingAccount: "å‰ã«ä½œã£ãŸã‚¢ã‚«ã‚¦ãƒ³ãƒˆ" +regenerate: "ã‚‚ã£ãºã‚“生æˆã™ã‚‹ã§" +fontSize: "å—ã®å¤§ãã•" noFollowRequests: "フォãƒãƒ¼ç”³è«‹ã¯ã‚らã¸ã‚“ã§" -openImageInNewTab: "ç”»åƒã‚’æ–°ã—ã„タブã§é–‹ã" +openImageInNewTab: "ç”»åƒã‚’æ–°ã—ã„タブã§é–‹ãã§" dashboard: "ダッシュボード" local: "ãƒãƒ¼ã‚«ãƒ«" remote: "リモート" @@ -504,7 +507,7 @@ objectStorageUseProxy: "Proxyを使ã†" objectStorageUseProxyDesc: "API接続ã«proxy使ã‚ã‚“ã®ã‚„ã£ãŸã‚‰åˆ‡ã£ã¦ãれã¸ã‚“?" objectStorageSetPublicRead: "アップãƒãƒ¼ãƒ‰ã—ãŸæ™‚ã«'public-read'ã‚’è¨å®šã—ã¦ã‚„" serverLogs: "サーãƒãƒ¼ãƒã‚°" -deleteAll: "å…¨ã¦å‰Šé™¤ã—ã¦ã‚„" +deleteAll: "全部ã»ã‹ã™" showFixedPostForm: "タイムラインã®ä¸Šã®æ–¹ã§æŠ•稿ã§ãるよã†ã«ã‚„ã£ã¦ãれã¸ã‚“?" showFixedPostFormInChannel: "タイムラインã®ä¸Šã®æ–¹ã§æŠ•稿ã§ãるよã†ã«ã™ã‚‹ã‚(ãƒãƒ£ãƒ³ãƒãƒ«)" newNoteRecived: "æ–°ã—ã„ノートãŒã‚ã‚‹ã§" @@ -514,11 +517,11 @@ listen: "è´ã" none: "ãªã—" showInPage: "ページã§è¡¨ç¤º" popout: "ãƒãƒƒãƒ—アウト" -volume: "音é‡" -masterVolume: "全体ã®éŸ³é‡" +volume: "ã‚„ã‹ã¾ã—ã•" +masterVolume: "全体ã®ã‚„ã‹ã¾ã—ã•" details: "ã‚‚ã£ã¨" chooseEmoji: "絵文å—ã‚’é¸ã¶" -unableToProcess: "ãªã‚“ã‹ä½œæ¥ãŒæ¢ã¾ã£ã¦ã—ã¾ã£ãŸã‚ˆã†ã‚„ã" +unableToProcess: "ãªã‚“ã‹å¥¥ã®æ–¹ã§è©°ã¾ã£ã¦ã‚‚ã†ãŸ" recentUsed: "最近使ã£ãŸã‚„ã¤" install: "インストール" uninstall: "アンインストール" @@ -536,14 +539,18 @@ output: "出力" script: "スクリプト" disablePagesScript: "Pagesã®ã‚¹ã‚¯ãƒªãƒ—トを無効ã«ã—ã¦ã‚„" updateRemoteUser: "ãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼æƒ…å ±ã®æ›´æ–°ã—ã¦ãれん?" -deleteAllFiles: "ã™ã¹ã¦ã®ãƒ•ァイルを削除" -deleteAllFilesConfirm: "ホンマã«ã™ã¹ã¦ã®ãƒ•ァイルを削除ã™ã‚‹ã‚“?消ã—ãŸã‚‚ã‚“ã¯ã‚‚ã†æˆ»ã£ã¦ã“ã‚“ã®ã‚„ã§ï¼Ÿ" +deleteAllFiles: "ファイルを全部ã»ã‹ã™" +deleteAllFilesConfirm: "ホンマã«ãƒ•ァイル全部ã»ã‹ã™ã‚“ã‹ï¼Ÿæ¶ˆã—ãŸã‚‚ã‚“ã¯ã‚‚ã†æˆ»ã£ã¦ã“ã‚“ã®ã‚„ã§ï¼Ÿ" removeAllFollowing: "フォãƒãƒ¼ã‚’全解除" removeAllFollowingDescription: "{host}ã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã‚’ã™ã¹ã¦è§£é™¤ã™ã‚‹ã§ã€‚ãã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ãŒæ¶ˆãˆã¦ç„¡ããªã£ãŸæ™‚ã¨ã‹ã«ã¯ä¾¿åˆ©ãªæ©Ÿèƒ½ã‚„ã§ã€‚" userSuspended: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯...å‡çµã•れã¨ã‚‹ã€‚" userSilenced: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯...サイレンスã•れã¨ã‚‹ã€‚" yourAccountSuspendedTitle: "ã‚ã‚“ãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆå‡çµã•れã¨ã‚‹ã§" yourAccountSuspendedDescription: "ã‚ã‚“ãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ã€ã‚µãƒ¼ãƒãƒ¼ã®åˆ©ç”¨è¦ç´„ã«é•åã—ãŸã¨ã‹ã®ç†ç”±ã§ã€å‡çµã•れã¨ã‚‹ã§ã€‚ç´°ã‹ã„ã“ã¨ã¯ç®¡ç†è€…ã¾ã§ãŠå•ã„åˆã‚ã›ãŸã£ã¦ãªãƒ¼ã€‚çµ¶å¯¾ã«æ–°ã—ã„アカウント作ã£ãŸã‚‰ã‚ã‹ã‚“ã§ã€‚絶対やã§ã€‚" +tokenRevoked: "トークンãŒç„¡åŠ¹ã‚„ã§" +tokenRevokedDescription: "ãƒã‚°ã‚¤ãƒ³ãƒˆãƒ¼ã‚¯ãƒ³ãŒå¤±åйã—ã¨ã‚‹ã§ã€‚ã‚‚ã£ã‹ã„ãƒã‚°ã‚¤ãƒ³ã—ã¦ã‚‚ã‚ã¦ã‚‚ãˆãˆã‹ï¼Ÿ" +accountDeleted: "アカウントã¯å‰Šé™¤ã•れã¨ã‚‹ã§" +accountDeletedDescription: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯å‰Šé™¤ã•れã¨ã‚‹ã§ã€‚" menu: "メニュー" divider: "分割線" addItem: "é …ç›®ã‚’è¿½åŠ " @@ -566,7 +573,7 @@ description: "説明" describeFile: "ã‚ャプションを付ã‘ã‚‹" enterFileDescription: "ã‚ャプションを入力" author: "作者" -leaveConfirm: "未ä¿å˜ã®å¤‰æ›´ãŒã‚ã‚‹ã§ï¼ã»ã‹ã—ã¦ãˆãˆã‹ï¼Ÿ" +leaveConfirm: "ã‚ã‚“ãŸã€ã„ã˜ã£ãŸã®ã«ã¾ã ä¿å˜ã—ã¦ãªã„ã§ï¼ã»ã‹ã—ã¦ãˆãˆã‹ï¼Ÿ" manage: "管ç†" plugins: "プラグイン" preferencesBackups: "è¨å®šã®ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—" @@ -600,12 +607,12 @@ smtpUser: "ユーザーå" smtpPass: "パスワード" emptyToDisableSmtpAuth: "ユーザーåã¨ãƒ‘スワードã«ãªã‚“も入れんã‹ã£ãŸã‚‰ã€SMTPèªè¨¼ã‚’無効化ã™ã‚‹ã§" smtpSecure: "SMTP æŽ¥ç¶šã«æš—黙的ãªSSL/TLSを使用ã™ã‚‹" -smtpSecureInfo: "STARTTLS使ã£ã¨ã‚‹æ™‚ã¯ã‚ªãƒ•ã«ã™ã‚‹ã§ã€‚" +smtpSecureInfo: "STARTTLS使ã£ã¨ã‚‹æ™‚ã¯ã‚ªãƒ•ã«ã—ã¦ã‚„。" testEmail: "é…信テスト" wordMute: "ワードミュート" regexpError: "æ£è¦è¡¨ç¾ã‚¨ãƒ©ãƒ¼" regexpErrorDescription: "{tab}ワードミュートã®{line}è¡Œç›®ã®æ£è¦è¡¨ç¾ã«ã‚¨ãƒ©ãƒ¼ãŒå‡ºã¦ããŸã§:" -instanceMute: "インスタンスミュート" +instanceMute: "サーãƒãƒ¼ãƒŸãƒ¥ãƒ¼ãƒˆ" userSaysSomething: "{name}ãŒä½•ã‹è¨€ã†ã¨ã‚‹ã‚" makeActive: "使ã†ã§" display: "表示" @@ -624,7 +631,7 @@ useGlobalSettingDesc: "オンã«ã™ã‚‹ã¨ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®é€šçŸ¥è¨å®šãŒä½¿ other: "ãã®ä»–" regenerateLoginToken: "ãƒã‚°ã‚¤ãƒ³ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å†ç”Ÿæˆ" regenerateLoginTokenDescription: "ãƒã‚°ã‚¤ãƒ³ã«ä½¿ã‚れる内部トークンをもã£ã‹ã„作るã§ã€‚ã„ã¤ã‚‚ãªã‚‰ã“れをやる必è¦ã¯ãªã„ã§ã€‚ã‚‚ã£ã‹ã„作るã¨ã€å…¨éƒ¨ã®ãƒ‡ãƒã‚¤ã‚¹ã§ãƒã‚°ã‚¢ã‚¦ãƒˆã•ã‚Œã‚‹ã§æ°—ãƒã¤ã‘ã¦ãªãƒ¼ã€‚" -setMultipleBySeparatingWithSpace: "スペースã§åŒºåˆ‡ã£ã¦è¤‡æ•°è¨å®šã§ãã‚‹ã§ã€‚" +setMultipleBySeparatingWithSpace: "スペースã§åŒºåˆ‡ã£ã¦ä½•個ã§ã‚‚è¨å®šã§ãã‚‹ã§ã€‚" fileIdOrUrl: "ファイルIDã‹URL" behavior: "動作" sample: "サンプル" @@ -636,7 +643,7 @@ abuseReported: "無事内容ãŒé€ä¿¡ã•れãŸã¿ãŸã„ã‚„ã§ã€‚ãŠãŠãã«ã€œã reporter: "é€šå ±è€…" reporteeOrigin: "é€šå ±å…ˆ" reporterOrigin: "é€šå ±å…ƒ" -forwardReport: "リモートインスタンスã«é€šå ±ã‚’転é€ã™ã‚‹ã§" +forwardReport: "リモートサーãƒãƒ¼ã«é€šå ±ã‚’転é€ã™ã‚‹ã§" forwardReportIsAnonymous: "リモートインスタンスã‹ã‚‰ã¯ã‚ã‚“ãŸã®æƒ…å ±ã¯è¦‹ã‚Œã¸ã‚“ãã£ã¦ã€åŒ¿åã®ã‚·ã‚¹ãƒ†ãƒ アカウントã¨ã—ã¦è¡¨ç¤ºã•れるã§ã€‚" send: "é€ä¿¡" abuseMarkAsResolved: "対応ã—ãŸã§" @@ -644,7 +651,7 @@ openInNewTab: "æ–°ã—ã„タブã§é–‹ã" openInSideView: "サイドビューã§é–‹ã" defaultNavigationBehaviour: "デフォルトã®ãƒŠãƒ“ゲーション" editTheseSettingsMayBreakAccount: "ã“ã®ã¸ã‚“ã®è¨å®šã‚’よã†ã‚ã‹ã‚‰ã‚“ã¾ã¾ã‚¤ã‚¸ã‚‹ã¨ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒå£Šã‚Œã¦ä½¿ãˆã‚“ããªã‚‹ã‹ã‚‚知れã¸ã‚“ã§ï¼Ÿ" -instanceTicker: "ノートã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹æƒ…å ±" +instanceTicker: "ノートã®ã‚µãƒ¼ãƒãƒ¼æƒ…å ±" waitingFor: "{x}ã‚’å¾…ã£ã¨ã‚‹ã§" random: "ランダム" system: "システム" @@ -655,7 +662,7 @@ createNew: "æ–°ã—ã作るã§" optional: "ä»»æ„" createNewClip: "æ–°ã—ã„クリップを作るã§" unclip: "クリップ解除ã™ã‚‹ã§" -confirmToUnclipAlreadyClippedNote: "ã“ã®ãƒŽãƒ¼ãƒˆã¯ã™ã§ã«ã‚¯ãƒªãƒƒãƒ—「{name}ã€ã«å«ã¾ã‚Œã¨ã‚‹ã§ã€‚ノートをã“ã®ã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰é™¤å¤–ã—ãŸã‚‹ï¼Ÿ" +confirmToUnclipAlreadyClippedNote: "ã“ã®ãƒŽãƒ¼ãƒˆã¯ã™ã§ã«ã‚¯ãƒªãƒƒãƒ—「{name}ã€ã«å«ã¾ã‚Œã¨ã‚‹ã§ã€‚ノートをã“ã®ã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰é™¤å¤–ã—よã‹ï¼Ÿ" public: "パブリック" i18nInfo: "Misskeyã¯æœ‰å¿—ã«ã‚ˆã£ã¦ã„ã‚ã‚“ãªè¨€èªžã«ç¿»è¨³ã•れã¨ã‚‹ã§ã€‚{link}ã§ç¿»è¨³ã«å”力ã—ãŸã£ã¦ã‚„ー。" manageAccessTokens: "アクセストークンã®ç®¡ç†" @@ -672,15 +679,15 @@ receivedReactionsCount: "リアクションã•ã‚ŒãŸæ•°" pollVotesCount: "ã‚¢ãƒ³ã‚±ãƒ¼ãƒˆã«æŠ•ç¥¨ã—ãŸæ•°" pollVotedCount: "ã‚¢ãƒ³ã‚±ãƒ¼ãƒˆã«æŠ•ç¥¨ã•ã‚ŒãŸæ•°" yes: "ãˆãˆã§" -no: "ã‚ã‹ã‚“ã§" +no: "ã‚ã‹ã‚“" driveFilesCount: "ドライブã®ãƒ•ァイル数" driveUsage: "ドライブ使用é‡ã‚„ã§" noCrawle: "クãƒãƒ¼ãƒ©ãƒ¼ã«ã‚ˆã‚‹ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã‚’æ‹’å¦ã™ã‚‹ã§" -noCrawleDescription: "検索エンジンã«ã‚ã‚“ãŸã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒšãƒ¼ã‚¸ã€ãƒŽãƒ¼ãƒˆã€Pagesã¨ã‹ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を登録(インデックス)ã›ã‡ã¸ã‚“よã†ã«é ¼ã‚€ã§ã€‚" +noCrawleDescription: "検索エンジンã«ã‚ã‚“ãŸã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒšãƒ¼ã‚¸ã€ãƒŽãƒ¼ãƒˆã€Pagesã¨ã‹ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を登録(インデックス)ã›ã‚“よã†ã«é ¼ã‚€ã§ã€‚邪é”ã™ã‚“ãã‚“ã‚„ã£ãŸã‚‰å¸°ã£ã¦ã€œã€‚" lockedAccountInfo: "フォãƒãƒ¼ã‚’承èªåˆ¶ã«ã—ã¨ã£ã¦ã‚‚ã€ãƒŽãƒ¼ãƒˆã®å…¬é–‹ç¯„囲を「フォãƒãƒ¯ãƒ¼ã€ã«ã›ã‡ã¸ã‚“é™ã‚Šã€èª°ã§ã‚‚ã‚ã‚“ãŸã®ãƒŽãƒ¼ãƒˆã‚’見れるã§ã€‚" alwaysMarkSensitive: "デフォルトã§ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’閲覧注æ„ã«ã™ã‚‹ã§" loadRawImages: "添付画åƒã®ã‚µãƒ ãƒã‚¤ãƒ«ã‚’オリジナル画質ã«ã™ã‚‹ã§" -disableShowingAnimatedImages: "アニメーション画åƒã‚’å†ç”Ÿã—ã‚„ã¸ã‚“ã§" +disableShowingAnimatedImages: "アニメーション画åƒã‚’å†ç”Ÿã›ã‚“ã¨ãã§" verificationEmailSent: "無事確èªã®ãƒ¡ãƒ¼ãƒ«ã‚’é€ã‚ŒãŸã§ã€‚ãƒ¡ãƒ¼ãƒ«ã«æ›¸ã„ã¦ã‚るリンクã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ã€è¨å®šã‚’完了ã—ã¦ãªãƒ¼ã€‚" notSet: "未è¨å®š" emailVerified: "メールアドレスã¯ç¢ºèªã•れãŸã§" @@ -690,14 +697,14 @@ pageLikedCount: "Pageã«ãˆãˆã‚„ã‚“ã¨æ€ã£ã¦ãã‚ŒãŸæ•°" contact: "連絡先" useSystemFont: "システムã®ãƒ‡ãƒ•ォルトã®ãƒ•ォントを使ã†ã§" clips: "クリップ" -experimentalFeatures: "実験的機能やã§" +experimentalFeatures: "ãŠãŸã‚ã—æ©Ÿèƒ½ã‚„ã§" developer: "開発者やã§" makeExplorable: "アカウントを見ã¤ã‘ã‚„ã™ãã™ã‚‹ã§" makeExplorableDescription: "オフã«ã™ã‚‹ã¨ã€ã€Œã¿ã¤ã‘ã‚‹ã€ã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒè¼‰ã‚‰ã‚“ããªã‚‹ã§ã€‚" showGapBetweenNotesInTimeline: "タイムラインã®ãƒŽãƒ¼ãƒˆã‚’離ã—ã¦è¡¨ç¤ºã™ã‚‹ã§" duplicate: "複製" left: "å·¦" -center: "ä¸å¤®" +center: "真んä¸" wide: "広ã„" narrow: "ç‹ã„" reloadToApplySetting: "è¨å®šã¯ãƒšãƒ¼ã‚¸ãƒªãƒãƒ¼ãƒ‰å¾Œã«åæ˜ ã•れるã§ã€‚今リãƒãƒ¼ãƒ‰ã—ã¨ãã‹ï¼Ÿ" @@ -708,7 +715,7 @@ onlineUsersCount: "{n}人ãŒèµ·ãã¨ã‚‹ã§" nUsers: "{n}ユーザー" nNotes: "{n}ノート" sendErrorReports: "エラーリãƒãƒ¼ãƒˆã‚’é€ã‚‹" -sendErrorReportsDescription: "オンã«ã—ãŸã‚‰ã€ãªã‚“ã‹å¤‰ãªã“ã¨ãŒèµ·ããŸã¨ãã«ã‚¨ãƒ©ãƒ¼ã®è©³ç´°ãŒMisskeyã«å…±æœ‰ã•れã¦ã€ã‚½ãƒ•トウェアã®å“質å‘上ã«å½¹ç«‹ã¦ã‚‰ã‚Œã‚‹ã‚“ã‚„ã€‚ã‚¨ãƒ©ãƒ¼æƒ…å ±ã«ã¯ã€OSã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€ãƒ–ラウザã®ç¨®é¡žã€è¡Œå‹•å±¥æ´ãªã©ãŒå«ã¾ã‚Œã‚‹ã§ã€‚" +sendErrorReportsDescription: "オンã«ã—ãŸã‚‰ã€å¤‰ãªã“ã¨ãŒèµ·ããŸã¨ãã«ã‚¨ãƒ©ãƒ¼ã®è©³ç´°ãŒMisskeyã«é€ã‚‰ã‚Œã¦ã€ã‚½ãƒ•トウェアã®å“質å‘上ã«ä½¿ãˆã‚‹ã‚ˆã†ã«ãªã‚‹ã§ã€‚ã‚¨ãƒ©ãƒ¼æƒ…å ±ã«ã¯ã€OSã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€ãƒ–ラウザã®ç¨®é¡žã€è¡Œå‹•å±¥æ´ãªã‚“ã‹ãŒå«ã¾ã‚Œã‚‹ã§ã€‚" myTheme: "マイテーマ" backgroundColor: "背景" accentColor: "アクセント" @@ -877,7 +884,7 @@ isSystemAccount: "システムãŒè‡ªå‹•ã§ä½œæˆãƒ»ç®¡ç†ã—ã¨ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒ typeToConfirm: "ã“ã®æ“作をやるんãªã‚‰ {x} ã¨å…¥åŠ›ã—ã¦ãªãƒ¼" deleteAccount: "アカウント削除ã™ã‚‹ã§" document: "ドã‚ュメント" -numberOfPageCache: "ページã‚ャッシュ数やã§" +numberOfPageCache: "ページã€ã©ã‚“ã ã‘ã‚ャッシュã™ã‚“ã®ï¼Ÿ" numberOfPageCacheDescription: "増やã™ã¨ä½¿ã„ã‚„ã™ããªã‚‹ã€è² è·ã¨ãƒ¡ãƒ¢ãƒªä½¿ç”¨é‡ãŒå¢—ãˆã¦ãã§ã€‚一長一çŸã‚„ãªã€‚" logoutConfirm: "ãƒã‚°ã‚¢ã‚¦ãƒˆã—ã¾ã£ã‹ï¼Ÿ" lastActiveDate: "最後ã«ä½¿ã£ãŸæ—¥æ™‚" @@ -947,7 +954,7 @@ thisPostMayBeAnnoying: "ã“ã®æŠ•ç¨¿ã¯è¿·æƒ‘ã‹ã‚‚ã—らんã§ã€‚" thisPostMayBeAnnoyingHome: "ãƒ›ãƒ¼ãƒ ã«æŠ•ç¨¿" thisPostMayBeAnnoyingCancel: "ã‚„ã‚ã¨ã" thisPostMayBeAnnoyingIgnore: "ã“ã®ã¾ã¾æŠ•稿" -collapseRenotes: "見ãŸã“ã¨ã‚ã‚‹Renoteã¯çœç•¥ã‚„ã§" +collapseRenotes: "見ãŸã“ã¨ã‚ã‚‹Renoteã¯é£›ã°ã—ã¦è¡¨ç¤ºã™ã‚‹ã§" internalServerError: "サーãƒãƒ¼å†…部エラー" internalServerErrorDescription: "サーãƒãƒ¼å†…部ã§ã‚ˆã†åˆ†ã‹ã‚‰ã‚“エラーやã‚" copyErrorInfo: "ã‚¨ãƒ©ãƒ¼æƒ…å ±ã‚’ã‚³ãƒ”ãƒ¼" @@ -959,6 +966,17 @@ invitationRequiredToRegister: "今ã“ã®ã‚µãƒ¼ãƒãƒ¼æ‹›å¾…制ã«ãªã£ã¦ã‚‚ã†ã emailNotSupported: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯ãƒ¡ãƒ¼ãƒ«é…ä¿¡ãŒã‚µãƒãƒ¼ãƒˆã•れã¦ã¸ã‚“ã¿ãŸã„ã‚„ã‚" postToTheChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã«æŠ•稿" cannotBeChangedLater: "後ã‹ã‚‰ã¯å¤‰ãˆã‚‰ã‚Œã¸ã‚“ã§ã€‚" +reactionAcceptance: "リアクションã®å—ã‘入れ" +likeOnly: "ã„ã„ãã ã‘" +likeOnlyForRemote: "リモートã‹ã‚‰ã¯ã„ã„ãã ã‘ãª" +rolesAssignedToMe: "自分ã«å‰²ã‚Šå½“ã¦ã‚‰ã‚ŒãŸãƒãƒ¼ãƒ«" +resetPasswordConfirm: "パスワード作り直ã™ã‚“ã§ãˆãˆãªï¼Ÿ" +sensitiveWords: "ã‘ã£ãŸã„ãªå˜èªž" +sensitiveWordsDescription: "è¨å®šã—ãŸå˜èªžãŒå…¥ã£ã¨ã‚‹ãƒŽãƒ¼ãƒˆã®å…¬é–‹ç¯„囲をホームã«ã—ãŸã‚‹ã‚。改行ã§åŒºåˆ‡ã£ãŸã‚‰è¤‡æ•°è¨å®šã§ãã‚‹ã§ã€‚" +notesSearchNotAvailable: "ノート検索ã¯ä½¿ã‚れã¸ã‚“ã§ã€‚" +license: "ライセンス" +unfavoriteConfirm: "ã»ã‚“ã¾ã«æ°—ã«å…¥ã‚‰ã‚“ã®ï¼Ÿ" +myClips: "自分ã®ã‚¯ãƒªãƒƒãƒ—" _achievements: earnedAt: "è²°ã£ãŸæ—¥ãƒ" _types: @@ -976,7 +994,7 @@ _achievements: title: "ノートã®ç”Ÿé§’å±±" description: "ノートを500回投稿ã—ãŸ" _notes1000: - title: "ノートã®å±±" + title: "ノートã®å…甲山" description: "ノートを1,000回投稿ã—ãŸ" _notes5000: title: "箕é¢ã®æ»ã‹ã‚‰ãƒŽãƒ¼ãƒˆ" @@ -1218,6 +1236,8 @@ _role: iconUrl: "アイコン画åƒã®URL" asBadge: "ãƒãƒƒã‚¸ã¨ã—ã¦è¦‹ã›ã‚‹" descriptionOfAsBadge: "オンã«ã™ã‚‹ã¨ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼åã®æ¨ªã‚“ã¨ã“ã«ãƒãƒ¼ãƒ«ã®ã‚¢ã‚¤ã‚³ãƒ³ãŒè¡¨ç¤ºã•れるã§ã€‚" + displayOrder: "è¡¨ç¤ºé †" + descriptionOfDisplayOrder: "æ•°ãŒã§ã‹ã„ã»ã©ã€UI上ã§å…ˆã«è¡¨ç¤ºã•れるã§ã€‚" canEditMembersByModerator: "モデレーターã®ãƒ¡ãƒ³ãƒãƒ¼ç·¨é›†ã‚’許å¯" descriptionOfCanEditMembersByModerator: "オンã«ã™ã‚‹ã¨ã€ç®¡ç†è€…ã«åŠ ãˆã¦ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ã‚‚ã“ã®ãƒãƒ¼ãƒ«ã¸ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’アサイン/アサイン解除ã§ãるよã†ã«ãªã‚‹ã§ã€‚オフã«ã™ã‚‹ã¨ç®¡ç†è€…ã®ã¿ãŒè¡Œãˆã‚‹ã§ã€‚" priority: "優先度" @@ -1243,6 +1263,7 @@ _role: rateLimitFactor: "レートリミット" descriptionOfRateLimitFactor: "ã¡ã£ã¡ã‚ƒã„ã»ã©åˆ¶é™ãŒç·©ããªã£ã¦ã€å¤§ãã„ã»ã©åˆ¶é™ã•れるã§ã€‚" canHideAds: "広告を表示ã•ã›ã¸ã‚“" + canSearchNotes: "ノート検索を使ã‚ã™ã‹ã©ã†ã‹" _condition: isLocal: "ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼" isRemote: "リモートユーザー" @@ -1377,7 +1398,7 @@ _wordMute: mutedNotes: "ミュートã•れãŸãƒŽãƒ¼ãƒˆ" _instanceMute: instanceMuteDescription: "ミュートã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¸ã®è¿”ä¿¡ã‚’å«ã‚ã¦ã€è¨å®šã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®å…¨ã¦ã®ãƒŽãƒ¼ãƒˆã¨Renoteをミュートã«ã™ã‚‹ã§ã€‚" - instanceMuteDescription2: "改行ã§åŒºåˆ‡ã£ã¦è¨å®šã™ã‚‹ã§" + instanceMuteDescription2: "改行ã§åŒºåˆ‡ã£ã¦è¨å®šã™ã‚‹ã‚“ã‚„ã§" title: "è¨å®šã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ãƒŽãƒ¼ãƒˆã‚’éš ã™ã§ã€‚" heading: "ミュートã™ã‚‹ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹" _theme: @@ -1464,7 +1485,7 @@ _sfx: channel: "ãƒãƒ£ãƒ³ãƒãƒ«é€šçŸ¥" _ago: future: "未æ¥" - justNow: "ãŸã£ãŸä»Š" + justNow: "ã¤ã„ã•ã£ã" secondsAgo: "{n}ç§’å‰" minutesAgo: "{n}分å‰" hoursAgo: "{n}時間å‰" @@ -1587,7 +1608,7 @@ _weekday: saturday: "土曜日" _widgets: profile: "プãƒãƒ•ィール" - instanceInfo: "ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹æƒ…å ±" + instanceInfo: "サーãƒãƒ¼æƒ…å ±" memo: "付箋" notifications: "通知" timeline: "タイムライン" @@ -1690,7 +1711,7 @@ _charts: apRequest: "リクエスト" usersIncDec: "ユーザーã®å¢—減" usersTotal: "ユーザーã®åˆè¨ˆ" - activeUsers: "アクティブユーザー数" + activeUsers: "ã„ã¾ãŠã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°" notesIncDec: "ノートã®å¢—減" localNotesIncDec: "ãƒãƒ¼ã‚«ãƒ«ã®ãƒŽãƒ¼ãƒˆã®å¢—減" remoteNotesIncDec: "リモートã®ãƒŽãƒ¼ãƒˆã®å¢—減" @@ -1844,3 +1865,6 @@ _deck: _dialog: charactersExceeded: "æœ€å¤§ã®æ–‡å—数を上回ã£ã¨ã‚‹ã§ï¼ä»Šã¯ {current} / 最大ã§ã‚‚ {max}" charactersBelow: "最å°ã®æ–‡å—数を下回ã£ã¨ã‚‹ã§ï¼ä»Šã¯ {current} / 最低ã§ã‚‚ {min}" +_disabledTimeline: + title: "使ã‚れã¸ã‚“タイムライン" + description: "ã‚ã‚“ãŸã®ä»Šã®ãƒãƒ¼ãƒ«ã‚„ã£ãŸã‚‰ã€ã“ã®ã‚¿ã‚¤ãƒ ラインã¯ä½¿ã‚れã¸ã‚“ã§ã€‚" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 9115afe5a5..e52d619f8c 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -122,6 +122,8 @@ unmarkAsSensitive: "ì—´ëžŒì£¼ì˜ í•´ì œ" enterFileName: "파ì¼ëª…ì„ ìž…ë ¥" mute: "뮤트" unmute: "뮤트 í•´ì œ" +renoteMute: "리노트를 뮤트" +renoteUnmute: "리노트 뮤트 í•´ì œ" block: "차단" unblock: "차단 í•´ì œ" suspend: "ì •ì§€" @@ -153,6 +155,7 @@ flagShowTimelineReplies: "타임ë¼ì¸ì— ë…¸íŠ¸ì˜ ë‹µê¸€ì„ í‘œì‹œí•˜ê¸°" flagShowTimelineRepliesDescription: "ì´ ì„¤ì •ì„ í™œì„±í™”í•˜ë©´ 타임ë¼ì¸ì— 다른 ìœ ì € ê°„ì˜ ë‹µê¸€ì„ í‘œì‹œí•©ë‹ˆë‹¤." autoAcceptFollowed: "팔로우 ì¤‘ì¸ ìœ ì €ë¡œë¶€í„°ì˜ íŒ”ë¡œìš° ìš”ì²ì„ ìžë™ 수ë½" addAccount: "ê³„ì • 추가" +reloadAccountsList: "ê³„ì • 리스트 ì •ë³´ ê°±ì‹ " loginFailed: "로그ì¸ì— 실패했습니다" showOnRemote: "리모트ì—서 보기" general: "ì¼ë°˜" @@ -506,6 +509,7 @@ objectStorageSetPublicRead: "ì—…ë¡œë“œí• ë•Œ 'public-read'를 ì„¤ì •í•˜ê¸°" serverLogs: "서버 로그" deleteAll: "ëª¨ë‘ ì‚ì œ" showFixedPostForm: "타임ë¼ì¸ ìƒë‹¨ì— 글 ìž‘ì„±ëž€ì„ í‘œì‹œ" +showFixedPostFormInChannel: "ì±„ë„ íƒ€ìž„ë¼ì¸ ìƒë‹¨ì— 글 ìž‘ì„±ëž€ì„ í‘œì‹œ" newNoteRecived: "새 노트가 있습니다" sounds: "소리" sound: "소리" @@ -543,6 +547,8 @@ userSuspended: "ì´ ê³„ì •ì€ ì •ì§€ëœ ìƒíƒœìž…니다." userSilenced: "ì´ ê³„ì •ì€ ì‚¬ì¼ëŸ°ìŠ¤ëœ ìƒíƒœìž…니다." yourAccountSuspendedTitle: "ê³„ì •ì´ ì •ì§€ë˜ì—ˆìŠµë‹ˆë‹¤" yourAccountSuspendedDescription: "ì´ ê³„ì •ì€ ì„œë²„ì˜ ì´ìš© ì•½ê´€ì„ ìœ„ë°˜í•˜ê±°ë‚˜, 기타 다른 ì´ìœ 로 ì¸í•´ ì •ì§€ë˜ì—ˆìŠµë‹ˆë‹¤. ìžì„¸í•œ 사í•ì€ ê´€ë¦¬ìžì—게 문ì˜í•´ 주ì‹ì‹œì˜¤. ê³„ì •ì„ ìƒˆë¡œ ìƒì„±í•˜ì§€ 마ì‹ì‹œì˜¤." +accountDeleted: "ê³„ì •ì´ ì •ì§€ë˜ì—ˆìŠµë‹ˆë‹¤" +accountDeletedDescription: "ì´ ê³„ì •ì´ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤." menu: "메뉴" divider: "êµ¬ë¶„ì„ " addItem: "í•목 추가" @@ -955,6 +961,9 @@ exploreOtherServers: "다른 서버 둘러보기" letsLookAtTimeline: "타임ë¼ì¸ 구경하기" disableFederationWarn: "ì—°í•©ì´ ë¹„í™œì„±í™”ë©ë‹ˆë‹¤. ë¹„í™œì„±í™”í•´ë„ ê²Œì‹œë¬¼ì´ ë¹„ê³µê°œê°€ ë˜ì§€ëŠ” 않습니다. ëŒ€ë¶€ë¶„ì˜ ê²½ìš° ì´ ì˜µì…˜ì„ í™œì„±í™”í• í•„ìš”ê°€ 없습니다." invitationRequiredToRegister: "현재 ì´ ì„œë²„ëŠ” 비공개입니다. 회ì›ê°€ìž…ì„ í•˜ì‹œë ¤ë©´ 초대 코드가 필요합니다." +emailNotSupported: "ì´ ì„œë²„ì—서는 ë©”ì¼ ì „ì†¡ì„ ì§€ì›í•˜ì§€ 않습니다" +postToTheChannel: "채ë„ì— ê²Œì‹œí•˜ê¸°" +cannotBeChangedLater: "ë‚˜ì¤‘ì— ë³€ê²½í• ìˆ˜ 없습니다." _achievements: earnedAt: "달성 ì¼ì‹œ" _types: diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 7798582db8..73b36f8ec2 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -2,7 +2,7 @@ _lang_: "䏿–‡(简体)" headlineMisskey: "通过帖å连接在一起的网络" introMisskey: "欢迎ï¼Misskey是一个开æºçš„ã€åŽ»ä¸å¿ƒåŒ–的“微åšå®¢â€æœåŠ¡ã€‚\né€šè¿‡ç¼–å†™ã€Œå¸–æ–‡ã€æ¥å’Œå¤§å®¶åˆ†äº«ä½ 的以åŠä½ 周围的事情å§ï¼ðŸ“¡\n通过「回应ã€åŠŸèƒ½ï¼Œå¯ä»¥è®©ä½ 快速地对大家的帖文表达å馈ðŸ‘\næ¥æŽ¢ç´¢æ–°çš„ä¸–ç•Œå§ï¼ðŸš€" -poweredByMisskeyDescription: "{name} 由开æºå¹³å° <b>Misskey</b> 驱动(也被称为 Misskey æœåŠ¡å™¨ï¼‰" +poweredByMisskeyDescription: "{name} 是开æºå¹³å° <b>Misskey</b> çš„æœåŠ¡å™¨ä¹‹ä¸€ã€‚" monthAndDay: "{month}月 {day}æ—¥" search: "æœç´¢" notifications: "通知" @@ -122,6 +122,8 @@ unmarkAsSensitive: "å–æ¶ˆæ ‡è®°ä¸ºæ•感内容" enterFileName: "请输入文件å" mute: "å±è”½" unmute: "解除å±è”½" +renoteMute: "å±è”½è½¬å¸–" +renoteUnmute: "解除å±è”½è½¬å¸–" block: "拉黑" unblock: "å–æ¶ˆæ‹‰é»‘" suspend: "冻结" @@ -153,6 +155,7 @@ flagShowTimelineReplies: "在时间线上显示帖å的回å¤" flagShowTimelineRepliesDescription: "å¯ç”¨æ—¶ï¼Œæ—¶é—´çº¿é™¤äº†æ˜¾ç¤ºç”¨æˆ·çš„帖å外,还会显示其他用户对帖å的回å¤ã€‚" autoAcceptFollowed: "自动å…许关注者的关注" addAccount: "æ·»åŠ è´¦æˆ·" +reloadAccountsList: "更新账户列表" loginFailed: "登录失败" showOnRemote: "转到所在æœåŠ¡å™¨æ˜¾ç¤º" general: "常规设置" @@ -355,7 +358,7 @@ hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)" recaptcha: "reCAPTCHA" enableRecaptcha: "å¯ç”¨ reCAPTCHA\n(请注æ„, æ¤åŠŸèƒ½åœ¨ä¸å›½å¤§é™†ä¸å¯ç”¨. 如果å¯ç”¨, å¯èƒ½å¯¼è‡´æ— 法æ£å¸¸ä½¿ç”¨ç™»å½•或注册ç‰åŠŸèƒ½)" recaptchaSiteKey: "网站密钥" -recaptchaSecretKey: "reCAPTCHA 密钥" +recaptchaSecretKey: "reCAPTCHA 密钥(SecretKey)" turnstile: "Turnstile" enableTurnstile: "å¯ç”¨Turnstile" turnstileSiteKey: "网站密钥" @@ -489,7 +492,7 @@ showFeaturedNotesInTimeline: "在时间线上显示çƒé—¨æŽ¨è" objectStorage: "对象å˜å‚¨" useObjectStorage: "使用对象å˜å‚¨" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "用于引用的URL。如果您æ£åœ¨ä½¿ç”¨CDN或åå‘代ç†ï¼Œè¯·æŒ‡å®šå…¶URL,例如S3:“https://<bucket>.s3.amazonaws.comâ€ï¼ŒGCS:“https://storage.googleapis.com/<bucket>â€" +objectStorageBaseUrlDesc: "这里是用于å‚考的URL,如果您æ£åœ¨ä½¿ç”¨CDN或åå‘代ç†ï¼Œè¯·æŒ‡å®šå…¶URL,例如S3:“https://<bucket>.s3.amazonaws.comâ€ï¼ŒGCS:“https://storage.googleapis.com/<bucket>â€" objectStorageBucket: "å˜å‚¨æ¡¶" objectStorageBucketDesc: "请指定使用的对象å˜å‚¨æœåŠ¡çš„å˜å‚¨æ¡¶å称。" objectStoragePrefix: "å‰ç¼€" @@ -544,6 +547,10 @@ userSuspended: "该用户已被冻结。" userSilenced: "该用户已被ç¦è¨€ã€‚" yourAccountSuspendedTitle: "账户已被冻结" yourAccountSuspendedDescription: "由于è¿å了æœåŠ¡å™¨çš„æœåŠ¡æ¡æ¬¾æˆ–å…¶ä»–åŽŸå› ï¼Œè¯¥è´¦æˆ·å·²è¢«å†»ç»“ã€‚ 您å¯ä»¥ä¸Žç®¡ç†å‘˜è”系以了解更多信æ¯ã€‚ 请ä¸è¦åˆ›å»ºä¸€ä¸ªæ–°çš„账户。" +tokenRevoked: "ä»¤ç‰Œæ— æ•ˆ" +tokenRevokedDescription: "登录令牌已ç»å¤±æ•ˆã€‚è¯·é‡æ–°ç™»å½•。" +accountDeleted: "叿ˆ·å·²åˆ 除" +accountDeletedDescription: "æ¤å¸æˆ·å·²ç»è¢«åˆ 除。" menu: "èœå•" divider: "分割线" addItem: "æ·»åŠ é¡¹ç›®" @@ -959,6 +966,17 @@ invitationRequiredToRegister: "æ¤æœåŠ¡å™¨ç›®å‰åªå…许拥有邀请ç 的人æ emailNotSupported: "æ¤æœåС噍䏿”¯æŒå‘é€é‚®ä»¶" postToTheChannel: "å‘布到频é“" cannotBeChangedLater: "之åŽä¸èƒ½å†æ›´æ”¹ã€‚" +reactionAcceptance: "接å—表情回应" +likeOnly: "仅点赞" +likeOnlyForRemote: "远程仅点赞" +rolesAssignedToMe: "指派给自己的角色" +resetPasswordConfirm: "确定é‡ç½®å¯†ç ?" +sensitiveWords: "æ•æ„Ÿè¯" +sensitiveWordsDescription: "将包å«è®¾ç½®è¯çš„帖åçš„å¯è§èŒƒå›´è®¾ç½®ä¸ºé¦–页。å¯ä»¥é€šè¿‡ç”¨æ¢è¡Œç¬¦åˆ†éš”æ¥è®¾ç½®å¤šä¸ªã€‚" +notesSearchNotAvailable: "取忣€ç´¢ä¸å¯ç”¨" +license: "许å¯ä¿¡æ¯" +unfavoriteConfirm: "确定è¦å–消收è—å—?" +myClips: "我的便ç¾" _achievements: earnedAt: "è¾¾æˆæ—¶é—´" _types: @@ -1218,6 +1236,8 @@ _role: iconUrl: "å›¾æ ‡URL" asBadge: "ä½œä¸ºå¾½ç« æ˜¾ç¤º" descriptionOfAsBadge: "å¼€å¯åŽï¼Œç”¨æˆ·åæ—è¾¹å°†ä¼šå‡ºçŽ°è§’è‰²å›¾æ ‡ã€‚" + displayOrder: "显示顺åº" + descriptionOfDisplayOrder: "æ•°å—越大,显示ä½ç½®è¶Šé å‰ã€‚" canEditMembersByModerator: "å…许监察者编辑æˆå‘˜" descriptionOfCanEditMembersByModerator: "如果选ä¸ï¼Œç›‘察者和管ç†å‘˜éƒ½èƒ½å¤Ÿä¸ºç”¨æˆ·åˆ†é…/å–æ¶ˆåˆ†é…角色。如果未选ä¸ï¼Œåˆ™åªæœ‰ç®¡ç†å‘˜å¯ä»¥æ‰§è¡Œæ¤æ“作。" priority: "优先级" @@ -1243,6 +1263,7 @@ _role: rateLimitFactor: "速率é™åˆ¶" descriptionOfRateLimitFactor: "值越å°é™åˆ¶è¶Šå°‘,值越大é™åˆ¶è¶Šå¤šã€‚" canHideAds: "å¯ä»¥éšè—广告" + canSearchNotes: "是å¦å¯ä»¥æœç´¢å¸–å" _condition: isLocal: "是本地用户" isRemote: "是远程用户" @@ -1517,7 +1538,7 @@ _2fa: step4: "从现在开始,任何登录æ“ä½œéƒ½å°†è¦æ±‚您æä¾›åЍæ€å£ä»¤ã€‚" securityKeyNotSupported: "您的æµè§ˆå™¨ä¸æ”¯æŒå®‰å…¨å¯†é’¥ã€‚" registerTOTPBeforeKey: "è¦æ³¨å†Œå®‰å…¨å¯†é’¥æˆ–Passkey,请先设置验è¯å™¨åº”用程åºã€‚" - securityKeyInfo: "您å¯ä»¥è®¾ç½®ä½¿ç”¨æ”¯æŒFIDO2的硬件安全密钥ã€è®¾å¤‡ä¸Šçš„æŒ‡çº¹æˆ–PINæ¥ä¿æŠ¤æ‚¨çš„登录过程。" + securityKeyInfo: "注册兼容 WebAuthn çš„å¯†é’¥ï¼Œä¾‹å¦‚æ”¯æŒ FIDO2 的硬件安全密钥ã€è®¾å¤‡ä¸Šçš„生物识别功能ã€PIN ç ä»¥åŠ Passkey ç‰ã€‚" chromePasskeyNotSupported: "ç›®å‰ä¸æ”¯æŒ Chrome çš„Passkey。" registerSecurityKey: "注册安全密钥或Passkey" securityKeyName: "输入密钥åç§°" @@ -1844,3 +1865,6 @@ _deck: _dialog: charactersExceeded: "å·²ç»è¶…过了最大å—符数! 当å‰å—符数 {current} / é™åˆ¶å—符数 {max}" charactersBelow: "低于最å°å—符数ï¼å½“å‰å—符数 {current} / é™åˆ¶å—符数 {min}" +_disabledTimeline: + title: "时间线已ç¦ç”¨" + description: "您ä¸èƒ½åœ¨å½“å‰è§’色使用时间线。" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 5c1512bd48..6109bdbeec 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -122,14 +122,16 @@ unmarkAsSensitive: "å–æ¶ˆæ¨™è¨˜ç‚ºæ•感內容" enterFileName: "請輸入檔案å稱" mute: "éœéŸ³" unmute: "解除éœéŸ³" +renoteMute: "將轉發貼文éœéŸ³" +renoteUnmute: "解除轉發貼文的éœéŸ³" block: "å°éŽ–" unblock: "解除å°éŽ–" suspend: "å‡çµ" unsuspend: "解除å‡çµ" blockConfirm: "確定è¦å°éŽ–æ¤ç”¨æˆ¶ï¼Ÿ" unblockConfirm: "確定解除å°éŽ–æ¤ç”¨æˆ¶ï¼Ÿ" -suspendConfirm: "確定å‡çµæ¤å¸³è™Ÿï¼Ÿ" -unsuspendConfirm: "ç¢ºå®šè§£å‡æ¤å¸³è™Ÿï¼Ÿ" +suspendConfirm: "確定å‡çµæ¤å¸³æˆ¶ï¼Ÿ" +unsuspendConfirm: "ç¢ºå®šè§£å‡æ¤å¸³æˆ¶ï¼Ÿ" selectList: "鏿“‡æ¸…å–®" selectChannel: "鏿“‡é »é“" selectAntenna: "鏿“‡å¤©ç·š" @@ -153,6 +155,7 @@ flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶å°å…¶ä»–貼文的回覆。" autoAcceptFollowed: "自動追隨ä¸ä½¿ç”¨è€…的追隨請求" addAccount: "æ·»åŠ å¸³æˆ¶" +reloadAccountsList: "更新帳戶清單的資訊" loginFailed: "登入失敗" showOnRemote: "轉到所在實例顯示" general: "一般" @@ -169,7 +172,7 @@ selectUser: "é¸å–使用者" recipient: "收件人" annotation: "註解" federation: "ç«™å°è¯é‚¦" -instances: "實例" +instances: "伺æœå™¨" registeredAt: "åˆæ¬¡è§€æ¸¬" latestRequestReceivedAt: "上次收到的請求" latestStatus: "最後狀態" @@ -403,7 +406,7 @@ securityKeyAndPasskey: "安全金鑰・Passkey" securityKey: "安全金鑰" lastUsed: "上次使用" lastUsedAt: "最後使用:{t}" -unregister: "註銷帳號" +unregister: "註銷帳戶" passwordLessLogin: "è¨ç½®ç„¡å¯†ç¢¼ç™»å…¥" passwordLessLoginDescription: "ä¸ä½¿ç”¨å¯†ç¢¼ï¼Œä»¥å®‰å…¨é‡‘鑰或 Passkey 登入" resetPassword: "é‡ç½®å¯†ç¢¼" @@ -544,6 +547,10 @@ userSuspended: "該使用者已被åœç”¨" userSilenced: "該用戶已被ç¦è¨€ã€‚" yourAccountSuspendedTitle: "帳戶已被å‡çµ" yourAccountSuspendedDescription: "由於é•å了伺æœå™¨çš„æœå‹™æ¢æ¬¾æˆ–å…¶ä»–åŽŸå› ï¼Œè©²å¸³æˆ¶å·²è¢«å‡çµã€‚ 您å¯ä»¥èˆ‡ç®¡ç†å“¡é€£ç¹«ä»¥äº†è§£æ›´å¤šè¨Šæ¯ã€‚ è«‹ä¸è¦å‰µå»ºä¸€å€‹æ–°çš„帳戶。" +tokenRevoked: "權æ–無效" +tokenRevokedDescription: "登入權æ–å¤±æ•ˆï¼Œè«‹é‡æ–°ç™»å…¥ã€‚" +accountDeleted: "帳戶已被刪除" +accountDeletedDescription: "這個帳戶已被刪除。" menu: "é¸å–®" divider: "分割線" addItem: "æ–°å¢žé …ç›®" @@ -872,10 +879,10 @@ recommended: "推薦" check: "檢查" driveCapOverrideLabel: "更改這個使用者的雲端硬碟容é‡ä¸Šé™" driveCapOverrideCaption: "如果指定0ä»¥ä¸‹çš„å€¼ï¼Œå°±æœƒè¢«å–æ¶ˆã€‚" -requireAdminForView: "å¿…é ˆä»¥ç®¡ç†å“¡å¸³è™Ÿç™»å…¥æ‰å¯ä»¥æª¢è¦–。" -isSystemAccount: "由系統自動建立與管ç†çš„帳號。" +requireAdminForView: "å¿…é ˆä»¥ç®¡ç†å“¡å¸³æˆ¶ç™»å…¥æ‰å¯ä»¥æª¢è¦–。" +isSystemAccount: "由系統自動建立與管ç†çš„帳戶。" typeToConfirm: "è¦åŸ·è¡Œé€™é …æ“作,請輸入 {x} " -deleteAccount: "刪除帳號" +deleteAccount: "刪除帳戶" document: "文件" numberOfPageCache: "å¿«å–é 颿•¸" numberOfPageCacheDescription: "å¢žåŠ æ•¸é‡æœƒæé«˜ä¾¿åˆ©æ€§ï¼Œä½†ä¹Ÿæœƒå¢žåŠ è² è·èˆ‡è¨˜æ†¶é«”使用é‡ã€‚" @@ -915,7 +922,7 @@ sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}ã€é€š windowMaximize: "最大化" windowRestore: "復原" caption: "標題" -loggedInAsBot: "以機器人帳號登入ä¸" +loggedInAsBot: "以機器人帳戶登入ä¸" tools: "工具" cannotLoad: "無法載入" numberOfProfileView: "個人檔案檢視次數" @@ -958,6 +965,14 @@ disableFederationWarn: "è¯é‚¦è¢«åœç”¨äº†ã€‚å³ä½¿åœç”¨ä¹Ÿä¸æœƒè®“æ‚¨çš„è²¼æ– invitationRequiredToRegister: "ç›®å‰é€™å€‹ä¼ºæœå™¨ç‚ºé‚€è«‹åˆ¶ï¼Œå¿…é ˆæ“æœ‰é‚€è«‹ç¢¼æ‰èƒ½è¨»å†Šã€‚" emailNotSupported: "這個伺æœå™¨ä¸æ”¯æ´å¯„é€éƒµä»¶" postToTheChannel: "ç™¼å¸ƒåˆ°é »é“" +cannotBeChangedLater: "之後ä¸èƒ½è®Šæ›´ã€‚" +reactionAcceptance: "接å—è¡¨æƒ…åæ‡‰" +likeOnly: "僅é™è®š" +likeOnlyForRemote: "é 端僅é™è®š" +rolesAssignedToMe: "指派給自己的角色" +resetPasswordConfirm: "é‡è¨å¯†ç¢¼ï¼Ÿ" +sensitiveWords: "æ•æ„Ÿè©ž" +sensitiveWordsDescription: "將嫿œ‰è¨å®šè©žå½™çš„貼文å¯è¦‹æ€§è¨ç‚ºç™¼é€è‡³é¦–é 。å¯ä»¥ç”¨æ›è¡Œä¾†é€²è¡Œè¤‡æ•¸çš„è¨å®šã€‚" _achievements: earnedAt: "ç²å¾—日期" _types: @@ -1217,6 +1232,8 @@ _role: iconUrl: "圖示的URL" asBadge: "é¡¯ç¤ºç‚ºå¾½ç« " descriptionOfAsBadge: "é–‹å•Ÿçš„è©±ï¼Œè§’è‰²åœ–ç¤ºæœƒé¡¯ç¤ºåœ¨ç”¨æˆ¶åæ—é‚Šã€‚" + displayOrder: "é¡¯ç¤ºé †åº" + descriptionOfDisplayOrder: "數å—越大,顯示在UI上的越上é¢ã€‚" canEditMembersByModerator: "å…許編輯審查員的æˆå“¡" descriptionOfCanEditMembersByModerator: "如果開啟,管ç†å“¡èˆ‡å¯©æŸ¥å“¡éƒ½å¯ä»¥ç‚ºä½¿ç”¨è€…指派/è§£é™¤æŒ‡æ´¾è©²è§’è‰²ã€‚å¦‚æžœé—œé–‰ï¼Œå‰‡åªæœ‰ç®¡ç†å“¡å¯ä»¥åŸ·è¡Œã€‚" priority: "優先級" @@ -1242,6 +1259,7 @@ _role: rateLimitFactor: "速率é™åˆ¶" descriptionOfRateLimitFactor: "值越å°é™åˆ¶è¶Šå°‘,值越大é™åˆ¶è¶Šå¤šã€‚" canHideAds: "ä¸é¡¯ç¤ºå»£å‘Š" + canSearchNotes: "å¯å¦æœå°‹è²¼æ–‡" _condition: isLocal: "本地使用者" isRemote: "é 端使用者" @@ -1713,7 +1731,7 @@ _instanceCharts: _timelines: home: "首é " local: "本地" - social: "社群" + social: "社交" global: "公開" _play: new: "新增Play" @@ -1843,3 +1861,6 @@ _deck: _dialog: charactersExceeded: "å·²è¶…éŽæœ€å¤§å—數ï¼ç¾åœ¨ {current} / é™åˆ¶ {max}" charactersBelow: "ä½Žæ–¼æœ€å°‘å—æ•¸ï¼ç¾åœ¨ {current} / é™åˆ¶ {max}" +_disabledTimeline: + title: "åœç”¨çš„æ™‚間軸" + description: "ç›®å‰çš„角色無法使用這個時間軸。" diff --git a/package.json b/package.json index d3e34e7821..f68608911c 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "13.9.2", + "version": "13.10.0", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@7.27.0", + "packageManager": "pnpm@7.29.3", "workspaces": [ "packages/frontend", "packages/backend", @@ -31,8 +31,8 @@ "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", "jest": "cd packages/backend && pnpm jest", "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", - "test": "pnpm jest", - "test-and-coverage": "pnpm jest-and-coverage", + "test": "pnpm -r test", + "test-and-coverage": "pnpm -r test-and-coverage", "format": "pnpm exec gulp format", "clean": "node ./scripts/clean.js", "clean-all": "node ./scripts/clean-all.js", @@ -55,12 +55,12 @@ "devDependencies": { "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", - "@typescript-eslint/eslint-plugin": "5.53.0", - "@typescript-eslint/parser": "5.53.0", + "@typescript-eslint/eslint-plugin": "5.54.1", + "@typescript-eslint/parser": "5.54.1", "cross-env": "7.0.3", "cypress": "12.7.0", "eslint": "8.35.0", - "start-server-and-test": "1.15.4" + "start-server-and-test": "2.0.0" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.2.0" diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html index 9ee5a95c05..a9ebf662fc 100644 --- a/packages/backend/assets/redoc.html +++ b/packages/backend/assets/redoc.html @@ -19,6 +19,6 @@ </head> <body> <redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc> - <script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.50/bundles/redoc.standalone.js" integrity="sha256-WJbngBWN9vp6vkEuzeoSj5tE5saW9Hfj6/SinkzhL2s=" crossorigin="anonymous"></script> + <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script> </body> </html> diff --git a/packages/backend/migration/1665091090561-add-renote-muting.js b/packages/backend/migration/1665091090561-add-renote-muting.js new file mode 100644 index 0000000000..d2ed2bd2e9 --- /dev/null +++ b/packages/backend/migration/1665091090561-add-renote-muting.js @@ -0,0 +1,16 @@ + +export class addRenoteMuting1665091090561 { + constructor() { + this.name = 'addRenoteMuting1665091090561'; + } + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "renote_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_renoteMuting_id" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `); + await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `); + } + + async down(queryRunner) { + } +} diff --git a/packages/backend/migration/1675053125067-fixforeignkeyreports.js b/packages/backend/migration/1675053125067-fixforeignkeyreports.js new file mode 100644 index 0000000000..ca5c10b11f --- /dev/null +++ b/packages/backend/migration/1675053125067-fixforeignkeyreports.js @@ -0,0 +1,15 @@ +export class fixforeignkeyreports1675053125067 { + name = 'fixforeignkeyreports1675053125067' + + async up(queryRunner) { + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`); + await queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT IF EXISTS "FK_a9021cc2e1feb5f72d3db6e9f5f"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`); + } +} diff --git a/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js b/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js new file mode 100644 index 0000000000..f1765dd146 --- /dev/null +++ b/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js @@ -0,0 +1,11 @@ +export class perNoteReactionAcceptance1678164627293 { + name = 'perNoteReactionAcceptance1678164627293' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "reactionAcceptance" character varying(64)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "reactionAcceptance"`); + } +} diff --git a/packages/backend/migration/1678426061773-tweak-varchar-length.js b/packages/backend/migration/1678426061773-tweak-varchar-length.js new file mode 100644 index 0000000000..984c41dba6 --- /dev/null +++ b/packages/backend/migration/1678426061773-tweak-varchar-length.js @@ -0,0 +1,68 @@ +export class tweakVarcharLength1678426061773 { + name = 'tweakVarcharLength1678426061773' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "name" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "maintainerName" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "maintainerEmail" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "langs" TYPE character varying(1024) array`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedUsers" TYPE character varying(1024) array`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "hiddenTags" TYPE character varying(1024) array`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "blockedHosts" TYPE character varying(1024) array`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "themeColor" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "mascotImageUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "bannerUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "backgroundImageUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "logoImageUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "errorImageUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "iconUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "hcaptchaSiteKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "hcaptchaSecretKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "recaptchaSiteKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "recaptchaSecretKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "turnstileSiteKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "turnstileSecretKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "summalyProxy" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "email" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpHost" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpUser" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpPass" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPublicKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPrivateKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "deeplAuthKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "ToSUrl" TO "termsOfServiceUrl"`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "termsOfServiceUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageBucket" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStoragePrefix" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageBaseUrl" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageEndpoint" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageRegion" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageAccessKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageSecretKey" TYPE character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "script" TYPE character varying(65536)`, undefined); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___readWrite" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___read" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___write" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredWithinWeek" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredWithinMonth" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredWithinYear" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredOutsideWeek" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredOutsideMonth" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredOutsideYear" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___readWrite" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___read" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___write" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredWithinWeek" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredWithinMonth" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredWithinYear" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredOutsideWeek" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredOutsideMonth" TYPE integer`); + await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredOutsideYear" TYPE integer`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "termsOfServiceUrl" TO "ToSUrl"`); + } +} diff --git a/packages/backend/migration/1678427401214-remove-unused.js b/packages/backend/migration/1678427401214-remove-unused.js new file mode 100644 index 0000000000..ee643e7776 --- /dev/null +++ b/packages/backend/migration/1678427401214-remove-unused.js @@ -0,0 +1,13 @@ +export class removeUnused1678427401214 { + name = 'removeUnused1678427401214' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedClipId"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedClipId" character varying(32)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`); + } +} diff --git a/packages/backend/migration/1678602320354-role-display-order.js b/packages/backend/migration/1678602320354-role-display-order.js new file mode 100644 index 0000000000..de8f6f1033 --- /dev/null +++ b/packages/backend/migration/1678602320354-role-display-order.js @@ -0,0 +1,11 @@ +export class roleDisplayOrder1678602320354 { + name = 'roleDisplayOrder1678602320354' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" ADD "displayOrder" integer NOT NULL DEFAULT '0'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "displayOrder"`); + } +} diff --git a/packages/backend/migration/1678694614599-sensitive-words.js b/packages/backend/migration/1678694614599-sensitive-words.js new file mode 100644 index 0000000000..6d4c5730c7 --- /dev/null +++ b/packages/backend/migration/1678694614599-sensitive-words.js @@ -0,0 +1,11 @@ +export class sensitiveWords1678694614599 { + name = 'sensitiveWords1678694614599' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveWords" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveWords"`); + } +} diff --git a/packages/backend/migration/1678869617549-retention-date-key.js b/packages/backend/migration/1678869617549-retention-date-key.js new file mode 100644 index 0000000000..1a31b9a750 --- /dev/null +++ b/packages/backend/migration/1678869617549-retention-date-key.js @@ -0,0 +1,14 @@ +export class retentionDateKey1678869617549 { + name = 'retentionDateKey1678869617549' + + async up(queryRunner) { + await queryRunner.query(`TRUNCATE TABLE "retention_aggregation"`, undefined); + await queryRunner.query(`ALTER TABLE "retention_aggregation" ADD "dateKey" character varying(512) NOT NULL`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f7c3576b37bd2eec966ae24477" ON "retention_aggregation" ("dateKey") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_f7c3576b37bd2eec966ae24477"`); + await queryRunner.query(`ALTER TABLE "retention_aggregation" DROP COLUMN "dateKey"`); + } +} diff --git a/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js new file mode 100644 index 0000000000..656a921770 --- /dev/null +++ b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js @@ -0,0 +1,11 @@ +export class addPropsForCustomEmoji1678945242650 { + name = 'addPropsForCustomEmoji1678945242650' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "license" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "license"`); + } +} diff --git a/packages/backend/migration/1678953978856-clip-favorite.js b/packages/backend/migration/1678953978856-clip-favorite.js new file mode 100644 index 0000000000..aa5dc93a6e --- /dev/null +++ b/packages/backend/migration/1678953978856-clip-favorite.js @@ -0,0 +1,23 @@ +export class clipFavorite1678953978856 { + name = 'clipFavorite1678953978856' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "clip_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_1b539f43906f05ebcabe752a977" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_25a31662b0b0cc9af6549a9d71" ON "clip_favorite" ("userId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b1754a39d0b281e07ed7c078ec" ON "clip_favorite" ("userId", "clipId") `); + await queryRunner.query(`ALTER TABLE "clip" ADD "lastClippedAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`CREATE INDEX "IDX_a3eac04ae2aa9e221e7596114a" ON "clip" ("lastClippedAt") `); + await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_fce61c7986cee54393e79f1d849" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_fce61c7986cee54393e79f1d849"`); + await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a3eac04ae2aa9e221e7596114a"`); + await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "lastClippedAt"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b1754a39d0b281e07ed7c078ec"`); + await queryRunner.query(`DROP INDEX "public"."IDX_25a31662b0b0cc9af6549a9d71"`); + await queryRunner.query(`DROP TABLE "clip_favorite"`); + } +} diff --git a/packages/backend/migration/1679309757174-antenna-active.js b/packages/backend/migration/1679309757174-antenna-active.js new file mode 100644 index 0000000000..69e845c142 --- /dev/null +++ b/packages/backend/migration/1679309757174-antenna-active.js @@ -0,0 +1,17 @@ +export class antennaActive1679309757174 { + name = 'antennaActive1679309757174' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now'`); + await queryRunner.query(`ALTER TABLE "antenna" ADD "isActive" boolean NOT NULL DEFAULT true`); + await queryRunner.query(`CREATE INDEX "IDX_084c2abb8948ef59a37dce6ac1" ON "antenna" ("lastUsedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_36ef5192a1ce55ed0e40aa4db5" ON "antenna" ("isActive") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_36ef5192a1ce55ed0e40aa4db5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_084c2abb8948ef59a37dce6ac1"`); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "isActive"`); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "lastUsedAt"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 35e8dc5c60..5a3dcfb5e7 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -23,29 +23,29 @@ }, "optionalDependencies": { "@swc/core-android-arm64": "^1.3.11", - "@swc/core-darwin-arm64": "^1.3.36", - "@swc/core-darwin-x64": "^1.3.36", - "@swc/core-linux-arm-gnueabihf": "^1.3.36", - "@swc/core-linux-arm64-gnu": "^1.3.36", - "@swc/core-linux-arm64-musl": "^1.3.36", - "@swc/core-linux-x64-gnu": "^1.3.36", - "@swc/core-linux-x64-musl": "^1.3.36", - "@swc/core-win32-arm64-msvc": "^1.3.36", - "@swc/core-win32-ia32-msvc": "^1.3.36", - "@swc/core-win32-x64-msvc": "^1.3.36", + "@swc/core-darwin-arm64": "^1.3.38", + "@swc/core-darwin-x64": "^1.3.38", + "@swc/core-linux-arm-gnueabihf": "^1.3.38", + "@swc/core-linux-arm64-gnu": "^1.3.38", + "@swc/core-linux-arm64-musl": "^1.3.38", + "@swc/core-linux-x64-gnu": "^1.3.38", + "@swc/core-linux-x64-musl": "^1.3.38", + "@swc/core-win32-arm64-msvc": "^1.3.38", + "@swc/core-win32-ia32-msvc": "^1.3.38", + "@swc/core-win32-x64-msvc": "^1.3.38", "@tensorflow/tfjs": "4.2.0", "@tensorflow/tfjs-node": "4.2.0" }, "dependencies": { - "@bull-board/api": "4.12.1", - "@bull-board/fastify": "4.12.1", - "@bull-board/ui": "4.12.1", + "@bull-board/api": "5.0.0", + "@bull-board/fastify": "5.0.0", + "@bull-board/ui": "5.0.0", "@discordapp/twemoji": "14.0.2", "@fastify/accepts": "4.1.0", "@fastify/cookie": "8.3.0", "@fastify/cors": "8.2.0", "@fastify/http-proxy": "8.4.0", - "@fastify/multipart": "7.4.1", + "@fastify/multipart": "7.4.2", "@fastify/static": "6.9.0", "@fastify/view": "7.4.1", "@nestjs/common": "9.3.9", @@ -54,7 +54,7 @@ "@peertube/http-signature": "1.7.0", "@sinonjs/fake-timers": "10.0.2", "@swc/cli": "0.1.62", - "@swc/core": "1.3.36", + "@swc/core": "1.3.38", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "5.3.1", @@ -74,12 +74,12 @@ "date-fns": "2.29.3", "deep-email-validator": "0.1.21", "escape-regexp": "0.0.1", - "fastify": "4.13.0", + "fastify": "4.14.1", "feed": "4.2.2", "file-type": "18.2.1", "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", - "got": "12.5.3", + "got": "12.6.0", "happy-dom": "8.9.0", "hpagent": "1.2.0", "ioredis": "4.28.5", @@ -102,7 +102,7 @@ "os-utils": "0.0.14", "otpauth": "^9.0.2", "parse5": "7.1.2", - "pg": "8.9.0", + "pg": "8.10.0", "private-ip": "3.0.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -128,10 +128,10 @@ "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", - "systeminformation": "5.17.10", + "systeminformation": "5.17.12", "tinycolor2": "1.6.0", "tmp": "0.2.1", - "tsc-alias": "1.8.2", + "tsc-alias": "1.8.3", "tsconfig-paths": "4.1.2", "twemoji-parser": "14.0.0", "typeorm": "0.3.11", @@ -146,7 +146,7 @@ "xev": "3.0.2" }, "devDependencies": { - "@jest/globals": "29.4.3", + "@jest/globals": "29.5.0", "@swc/jest": "0.2.24", "@types/accepts": "1.3.5", "@types/archiver": "5.3.1", @@ -164,7 +164,7 @@ "@types/jsonld": "1.5.8", "@types/jsrsasign": "10.5.5", "@types/mime-types": "2.1.1", - "@types/node": "18.14.1", + "@types/node": "18.15.0", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.7", "@types/oauth": "0.9.1", @@ -176,7 +176,7 @@ "@types/ratelimiter": "3.4.4", "@types/redis": "4.0.11", "@types/rename": "1.0.4", - "@types/sanitize-html": "2.8.0", + "@types/sanitize-html": "2.8.1", "@types/semver": "7.3.13", "@types/sharp": "0.31.1", "@types/sinonjs__fake-timers": "8.1.2", @@ -188,13 +188,13 @@ "@types/web-push": "3.3.2", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.52.0", - "@typescript-eslint/parser": "5.53.0", + "@typescript-eslint/eslint-plugin": "5.54.1", + "@typescript-eslint/parser": "5.54.1", "cross-env": "7.0.3", "eslint": "8.35.0", "eslint-plugin-import": "2.27.5", "execa": "6.1.0", - "jest": "29.4.3", - "jest-mock": "29.4.3" + "jest": "29.5.0", + "jest-mock": "29.5.0" } } diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 2ebee0f7e0..1ca38d8bb0 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -3,7 +3,7 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/index.js' import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; export const ACHIEVEMENT_TYPES = [ 'notes1', @@ -90,7 +90,7 @@ export class AchievementService { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, ) { } @@ -114,7 +114,7 @@ export class AchievementService { }], }); - this.createNotificationService.createNotification(userId, 'achievementEarned', { + this.notificationService.createNotification(userId, 'achievementEarned', { achievement: type, }); } diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 05930350fa..aaa26a8321 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -10,7 +10,7 @@ import { isUserRelated } from '@/misc/is-user-related.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; import * as Acct from '@/misc/acct.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -71,12 +71,14 @@ export class AntennaService implements OnApplicationShutdown { this.antennas.push({ ...body, createdAt: new Date(body.createdAt), + lastUsedAt: new Date(body.lastUsedAt), }); break; case 'antennaUpdated': this.antennas[this.antennas.findIndex(a => a.id === body.id)] = { ...body, createdAt: new Date(body.createdAt), + lastUsedAt: new Date(body.lastUsedAt), }; break; case 'antennaDeleted': @@ -217,7 +219,9 @@ export class AntennaService implements OnApplicationShutdown { @bindThis public async getAntennas() { if (!this.antennasFetched) { - this.antennas = await this.antennasRepository.find(); + this.antennas = await this.antennasRepository.findBy({ + isActive: true, + }); this.antennasFetched = true; } diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 491d8ab113..d67e80fc1d 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -5,7 +5,6 @@ import { AntennaService } from './AntennaService.js'; import { AppLockService } from './AppLockService.js'; import { AchievementService } from './AchievementService.js'; import { CaptchaService } from './CaptchaService.js'; -import { CreateNotificationService } from './CreateNotificationService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CustomEmojiService } from './CustomEmojiService.js'; import { DeleteAccountService } from './DeleteAccountService.js'; @@ -82,6 +81,7 @@ import { HashtagEntityService } from './entities/HashtagEntityService.js'; import { InstanceEntityService } from './entities/InstanceEntityService.js'; import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js'; import { MutingEntityService } from './entities/MutingEntityService.js'; +import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js'; import { NoteEntityService } from './entities/NoteEntityService.js'; import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js'; import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js'; @@ -125,7 +125,6 @@ const $AntennaService: Provider = { provide: 'AntennaService', useExisting: Ante const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; -const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService }; const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService }; @@ -203,6 +202,7 @@ const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useEx const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService }; const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService }; const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService }; +const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService }; const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService }; const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService }; const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService }; @@ -248,7 +248,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AppLockService, AchievementService, CaptchaService, - CreateNotificationService, CreateSystemUserService, CustomEmojiService, DeleteAccountService, @@ -325,6 +324,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting InstanceEntityService, ModerationLogEntityService, MutingEntityService, + RenoteMutingEntityService, NoteEntityService, NoteFavoriteEntityService, NoteReactionEntityService, @@ -365,7 +365,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AppLockService, $AchievementService, $CaptchaService, - $CreateNotificationService, $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, @@ -442,6 +441,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $InstanceEntityService, $ModerationLogEntityService, $MutingEntityService, + $RenoteMutingEntityService, $NoteEntityService, $NoteFavoriteEntityService, $NoteReactionEntityService, @@ -483,7 +483,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AppLockService, AchievementService, CaptchaService, - CreateNotificationService, CreateSystemUserService, CustomEmojiService, DeleteAccountService, @@ -559,6 +558,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting InstanceEntityService, ModerationLogEntityService, MutingEntityService, + RenoteMutingEntityService, NoteEntityService, NoteFavoriteEntityService, NoteReactionEntityService, @@ -599,7 +599,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AppLockService, $AchievementService, $CaptchaService, - $CreateNotificationService, $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, @@ -675,6 +674,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $InstanceEntityService, $ModerationLogEntityService, $MutingEntityService, + $RenoteMutingEntityService, $NoteEntityService, $NoteFavoriteEntityService, $NoteReactionEntityService, @@ -708,4 +708,4 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting //#endregion ], }) -export class CoreModule {} +export class CoreModule { } diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts deleted file mode 100644 index eba7171fb6..0000000000 --- a/packages/backend/src/core/CreateNotificationService.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { setTimeout } from 'node:timers/promises'; -import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; -import type { Notification } from '@/models/entities/Notification.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { IdService } from '@/core/IdService.js'; -import { DI } from '@/di-symbols.js'; -import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; -import { bindThis } from '@/decorators.js'; - -@Injectable() -export class CreateNotificationService implements OnApplicationShutdown { - #shutdownController = new AbortController(); - - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - @Inject(DI.notificationsRepository) - private notificationsRepository: NotificationsRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - private notificationEntityService: NotificationEntityService, - private idService: IdService, - private globalEventService: GlobalEventService, - private pushNotificationService: PushNotificationService, - ) { - } - - @bindThis - public async createNotification( - notifieeId: User['id'], - type: Notification['type'], - data: Partial<Notification>, - ): Promise<Notification | null> { - if (data.notifierId && (notifieeId === data.notifierId)) { - return null; - } - - const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); - - const isMuted = profile?.mutingNotificationTypes.includes(type); - - // Create notification - const notification = await this.notificationsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - notifieeId: notifieeId, - type: type, - // 相手ãŒã“ã®é€šçŸ¥ã‚’ミュートã—ã¦ã„るよã†ãªã‚‰ã€æ—¢èªã‚’予ã‚ã¤ã‘ã¦ãŠã - isRead: isMuted, - ...data, - } as Partial<Notification>) - .then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0])); - - const packed = await this.notificationEntityService.pack(notification, {}); - - // Publish notification event - this.globalEventService.publishMainStream(notifieeId, 'notification', packed); - - // 2秒経ã£ã¦ã‚‚(今回作æˆã—ãŸ)é€šçŸ¥ãŒæ—¢èªã«ãªã‚‰ãªã‹ã£ãŸã‚‰ã€Œæœªèªã®é€šçŸ¥ãŒã‚りã¾ã™ã‚ˆã€ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行ã™ã‚‹ - setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { - const fresh = await this.notificationsRepository.findOneBy({ id: notification.id }); - if (fresh == null) return; // æ—¢ã«å‰Šé™¤ã•れã¦ã„ã‚‹ã‹ã‚‚ã—れãªã„ - if (fresh.isRead) return; - - //#region ãŸã ã—ミュートã—ã¦ã„るユーザーã‹ã‚‰ã®é€šçŸ¥ãªã‚‰ç„¡è¦– - const mutings = await this.mutingsRepository.findBy({ - muterId: notifieeId, - }); - if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) { - return; - } - //#endregion - - this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); - this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); - - if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); - if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); - }, () => { /* aborted, ignore it */ }); - - return notification; - } - - // TODO - //const locales = await import('../../../../locales/index.js'); - - // TODO: locale ファイルをクライアント用ã¨ã‚µãƒ¼ãƒãƒ¼ç”¨ã§åˆ†ã‘ãŸã„ - - @bindThis - private async emailNotificationFollow(userId: User['id'], follower: User) { - /* - const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); - if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; - const locale = locales[userProfile.lang ?? 'ja-JP']; - const i18n = new I18n(locale); - // TODO: render user information html - sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); - */ - } - - @bindThis - private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { - /* - const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); - if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; - const locale = locales[userProfile.lang ?? 'ja-JP']; - const i18n = new I18n(locale); - // TODO: render user information html - sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); - */ - } - - onApplicationShutdown(signal?: string | undefined): void { - this.#shutdownController.abort(); - } -} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index a1a257fbd1..b404848d7d 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -44,6 +44,7 @@ export class CustomEmojiService { category: string | null; aliases: string[]; host: string | null; + license: string | null; }): Promise<Emoji> { const emoji = await this.emojisRepository.insert({ id: this.idService.genId(), @@ -55,10 +56,11 @@ export class CustomEmojiService { originalUrl: data.driveFile.url, publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, type: data.driveFile.webpublicType ?? data.driveFile.type, + license: data.license, }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); if (data.host == null) { - await this.db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache?.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiAdded', { emoji: await this.emojiEntityService.packDetailed(emoji.id), diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index f4a06faebb..f1e93d6dd9 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -2,6 +2,7 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { v4 as uuid } from 'uuid'; import sharp from 'sharp'; +import { sharpBmp } from 'sharp-read-bmp'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; @@ -33,8 +34,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { FileInfoService } from '@/core/FileInfoService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import type S3 from 'aws-sdk/clients/s3.js'; import { correctFilename } from '@/misc/correct-filename.js'; +import { isMimeImage } from '@/misc/is-mime-image.js'; +import type S3 from 'aws-sdk/clients/s3.js'; type AddFileArgs = { /** User who wish to add file */ @@ -274,8 +276,8 @@ export class DriveService { } } - if (!['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/svg+xml'].includes(type)) { - this.registerLogger.debug('web image and thumbnail not created (not an required file)'); + if (!isMimeImage(type, 'sharp-convertible-image-with-bmp')) { + this.registerLogger.debug('web image and thumbnail not created (cannot convert by sharp)'); return { webpublic: null, thumbnail: null, @@ -284,22 +286,16 @@ export class DriveService { let img: sharp.Sharp | null = null; let satisfyWebpublic: boolean; + let isAnimated: boolean; try { - img = sharp(path); + img = await sharpBmp(path, type); const metadata = await img.metadata(); - const isAnimated = metadata.pages && metadata.pages > 1; - - // skip animated - if (isAnimated) { - return { - webpublic: null, - thumbnail: null, - }; - } + isAnimated = !!(metadata.pages && metadata.pages > 1); satisfyWebpublic = !!( - type !== 'image/svg+xml' && type !== 'image/webp' && type !== 'image/avif' && + type !== 'image/svg+xml' && // security reason + type !== 'image/avif' && // not supported by Mastodon and MS Edge !(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) && metadata.width && metadata.width <= 2048 && metadata.height && metadata.height <= 2048 @@ -315,15 +311,13 @@ export class DriveService { // #region webpublic let webpublic: IImage | null = null; - if (generateWeb && !satisfyWebpublic) { + if (generateWeb && !satisfyWebpublic && !isAnimated) { this.registerLogger.info('creating web image'); try { if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) { - webpublic = await this.imageProcessingService.convertSharpToJpeg(img, 2048, 2048); - } else if (['image/png'].includes(type)) { - webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048); - } else if (['image/svg+xml'].includes(type)) { + webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048); + } else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) { webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048); } else { this.registerLogger.debug('web image not created (not an required image)'); @@ -333,6 +327,7 @@ export class DriveService { } } else { if (satisfyWebpublic) this.registerLogger.info('web image not created (original satisfies webpublic)'); + else if (isAnimated) this.registerLogger.info('web image not created (animated image)'); else this.registerLogger.info('web image not created (from remote)'); } // #endregion webpublic @@ -341,10 +336,10 @@ export class DriveService { let thumbnail: IImage | null = null; try { - if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(type)) { - thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 280); + if (isAnimated) { + thumbnail = await this.imageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 }); } else { - this.registerLogger.debug('thumbnail not created (not an required file)'); + thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422); } } catch (err) { this.registerLogger.warn('thumbnail not created (an error occured)', err as Error); @@ -476,7 +471,7 @@ export class DriveService { // DriveFile.nameã¯256æ–‡å—, validateFileNameã¯200æ–‡å—制é™ã§ã‚ã‚‹ãŸã‚〠// extã‚’ä»˜åŠ ã—ã¦ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã®æ–‡å—数制é™ã«å½“ãŸã‚‹ã“ã¨ã¯ã¾ãšãªã„ (name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled', - info.type.ext + info.type.ext, ); if (user && !force) { @@ -728,10 +723,20 @@ export class DriveService { const s3 = this.s3Service.getS3(meta); - await s3.deleteObject({ - Bucket: meta.objectStorageBucket!, - Key: key, - }).promise(); + try { + await s3.deleteObject({ + Bucket: meta.objectStorageBucket!, + Key: key, + }).promise(); + } catch (err: any) { + if (err.code === 'NoSuchKey') { + console.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err); + return; + } + throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, { + cause: err, + }); + } } @bindThis @@ -749,7 +754,7 @@ export class DriveService { }: UploadFromUrlArgs): Promise<DriveFile> { // Create temp file const [path, cleanup] = await createTemp(); - + try { // write content at URL to temp file const { filename: name } = await this.downloadService.downloadUrl(url, path); diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 65a69a0235..d261a6c657 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -16,7 +16,7 @@ import type { UserListStreamTypes, UserStreamTypes, } from '@/server/api/stream/types.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index 7c88f5e9a0..3246475d12 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -15,15 +15,28 @@ export type IImageStream = { type: string; }; -export type IImageStreamable = IImage | IImageStream; +export type IImageSharp = { + data: sharp.Sharp; + ext: string | null; + type: string; +}; + +export type IImageStreamable = IImage | IImageStream | IImageSharp; export const webpDefault: sharp.WebpOptions = { - quality: 85, + quality: 77, alphaQuality: 95, lossless: false, nearLossless: false, smartSubsample: true, mixed: true, + effort: 2, +}; + +export const avifDefault: sharp.AvifOptions = { + quality: 60, + lossless: false, + effort: 2, }; import { bindThis } from '@/decorators.js'; @@ -38,90 +51,96 @@ export class ImageProcessingService { } /** - * Convert to JPEG + * Convert to WebP * with resize, remove metadata, resolve orientation, stop animation */ @bindThis - public async convertToJpeg(path: string, width: number, height: number): Promise<IImage> { - return this.convertSharpToJpeg(await sharp(path), width, height); + public async convertToWebp(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { + return this.convertSharpToWebp(sharp(path), width, height, options); } @bindThis - public async convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> { - const data = await sharp + public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { + const result = this.convertSharpToWebpStream(sharp, width, height, options); + + return { + data: await result.data.toBuffer(), + ext: result.ext, + type: result.type, + }; + } + + @bindThis + public convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageSharp { + return this.convertSharpToWebpStream(sharp(path), width, height, options); + } + + @bindThis + public convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageSharp { + const data = sharp .resize(width, height, { fit: 'inside', withoutEnlargement: true, }) .rotate() - .jpeg({ - quality: 85, - progressive: true, - }) - .toBuffer(); + .webp(options); return { data, - ext: 'jpg', - type: 'image/jpeg', + ext: 'webp', + type: 'image/webp', }; } /** - * Convert to WebP + * Convert to Avif * with resize, remove metadata, resolve orientation, stop animation */ @bindThis - public async convertToWebp(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { - return this.convertSharpToWebp(sharp(path), width, height, options); + public async convertToAvif(path: string, width: number, height: number, options: sharp.AvifOptions = avifDefault): Promise<IImage> { + return this.convertSharpToAvif(sharp(path), width, height, options); } @bindThis - public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { - const data = await sharp - .resize(width, height, { - fit: 'inside', - withoutEnlargement: true, - }) - .rotate() - .webp(options) - .toBuffer(); + public async convertSharpToAvif(sharp: sharp.Sharp, width: number, height: number, options: sharp.AvifOptions = avifDefault): Promise<IImage> { + const result = this.convertSharpToAvifStream(sharp, width, height, options); return { - data, - ext: 'webp', - type: 'image/webp', + data: await result.data.toBuffer(), + ext: result.ext, + type: result.type, }; } @bindThis - public convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream { - return this.convertSharpToWebpStream(sharp(path), width, height, options); + public convertToAvifStream(path: string, width: number, height: number, options: sharp.AvifOptions = avifDefault): IImageSharp { + return this.convertSharpToAvifStream(sharp(path), width, height, options); } @bindThis - public convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream { + public convertSharpToAvifStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.AvifOptions = avifDefault): IImageSharp { const data = sharp .resize(width, height, { fit: 'inside', withoutEnlargement: true, }) .rotate() - .webp(options); + .avif(options); return { data, - ext: 'webp', - type: 'image/webp', + ext: 'avif', + type: 'image/avif', }; } + /** * Convert to PNG * with resize, remove metadata, resolve orientation, stop animation */ @bindThis public async convertToPng(path: string, width: number, height: number): Promise<IImage> { - return this.convertSharpToPng(await sharp(path), width, height); + return this.convertSharpToPng(sharp(path), width, height); } @bindThis diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 4c4261ba79..2fc2a3d54f 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -30,7 +30,7 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { AntennaService } from '@/core/AntennaService.js'; @@ -44,6 +44,7 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; +import { MetaService } from '@/core/MetaService.js'; const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); @@ -59,7 +60,7 @@ class NotificationManager { constructor( private mutingsRepository: MutingsRepository, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, notifier: { id: User['id']; }, note: Note, ) { @@ -100,7 +101,7 @@ class NotificationManager { // 通知ã•れるå´ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒé€šçŸ¥ã™ã‚‹å´ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ミュートã—ã¦ã„ãªã„é™ã‚Šã¯é€šçŸ¥ã™ã‚‹ if (!mentioneesMutedUserIds.includes(this.notifier.id)) { - this.createNotificationService.createNotification(x.target, x.reason, { + this.notificationService.createNotification(x.target, x.reason, { notifierId: this.notifier.id, noteId: this.note.id, }); @@ -125,6 +126,7 @@ type Option = { files?: DriveFile[] | null; poll?: IPoll | null; localOnly?: boolean | null; + reactionAcceptance?: Note['reactionAcceptance']; cw?: string | null; visibility?: string; visibleUsers?: MinimumUser[] | null; @@ -181,7 +183,7 @@ export class NoteCreateService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private queueService: QueueService, private noteReadService: NoteReadService, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private hashtagService: HashtagService, @@ -191,11 +193,12 @@ export class NoteCreateService implements OnApplicationShutdown { private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, + private metaService: MetaService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private activeUsersChart: ActiveUsersChart, private instanceChart: InstanceChart, - ) {} + ) { } @bindThis public async create(user: { @@ -229,7 +232,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.channel != null) data.localOnly = true; if (data.visibility === 'public' && data.channel == null) { - if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { + if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) { + data.visibility = 'home'; + } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; } } @@ -346,6 +351,7 @@ export class NoteCreateService implements OnApplicationShutdown { emojis, userId: user.id, localOnly: data.localOnly!, + reactionAcceptance: data.reactionAcceptance, visibility: data.visibility as any, visibleUserIds: data.visibility === 'specified' ? data.visibleUsers @@ -385,7 +391,7 @@ export class NoteCreateService implements OnApplicationShutdown { // æŠ•ç¨¿ã‚’ä½œæˆ try { if (insert.hasPoll) { - // Start transaction + // Start transaction await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.insert(Note, insert); @@ -408,7 +414,7 @@ export class NoteCreateService implements OnApplicationShutdown { return insert; } catch (e) { - // duplicate key error + // duplicate key error if (isDuplicateKeyValueError(e)) { const err = new Error('Duplicated note'); err.name = 'duplicated'; @@ -552,7 +558,7 @@ export class NoteCreateService implements OnApplicationShutdown { } }); - const nm = new NotificationManager(this.mutingsRepository, this.createNotificationService, user, note); + const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note); await this.createMentionedEvents(mentionedUsers, note, nm); diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index d23fb8238b..22d72815ec 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -4,7 +4,7 @@ import { In, IsNull, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { User } from '@/models/entities/User.js'; import type { Channel } from '@/models/entities/Channel.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { Note } from '@/models/entities/Note.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 88173c2307..48f2c65847 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -1,21 +1,37 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { setTimeout } from 'node:timers/promises'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { NotificationsRepository } from '@/models/index.js'; +import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { Notification } from '@/models/entities/Notification.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import { GlobalEventService } from './GlobalEventService.js'; -import { PushNotificationService } from './PushNotificationService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { PushNotificationService } from '@/core/PushNotificationService.js'; +import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; +import { IdService } from '@/core/IdService.js'; @Injectable() -export class NotificationService { +export class NotificationService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + @Inject(DI.notificationsRepository) private notificationsRepository: NotificationsRepository, + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private notificationEntityService: NotificationEntityService, private userEntityService: UserEntityService, + private idService: IdService, private globalEventService: GlobalEventService, private pushNotificationService: PushNotificationService, ) { @@ -67,4 +83,93 @@ export class NotificationService { private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds }); } + + @bindThis + public async createNotification( + notifieeId: User['id'], + type: Notification['type'], + data: Partial<Notification>, + ): Promise<Notification | null> { + if (data.notifierId && (notifieeId === data.notifierId)) { + return null; + } + + const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); + + const isMuted = profile?.mutingNotificationTypes.includes(type); + + // Create notification + const notification = await this.notificationsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + notifieeId: notifieeId, + type: type, + // 相手ãŒã“ã®é€šçŸ¥ã‚’ミュートã—ã¦ã„るよã†ãªã‚‰ã€æ—¢èªã‚’予ã‚ã¤ã‘ã¦ãŠã + isRead: isMuted, + ...data, + } as Partial<Notification>) + .then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0])); + + const packed = await this.notificationEntityService.pack(notification, {}); + + // Publish notification event + this.globalEventService.publishMainStream(notifieeId, 'notification', packed); + + // 2秒経ã£ã¦ã‚‚(今回作æˆã—ãŸ)é€šçŸ¥ãŒæ—¢èªã«ãªã‚‰ãªã‹ã£ãŸã‚‰ã€Œæœªèªã®é€šçŸ¥ãŒã‚りã¾ã™ã‚ˆã€ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行ã™ã‚‹ + setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { + const fresh = await this.notificationsRepository.findOneBy({ id: notification.id }); + if (fresh == null) return; // æ—¢ã«å‰Šé™¤ã•れã¦ã„ã‚‹ã‹ã‚‚ã—れãªã„ + if (fresh.isRead) return; + + //#region ãŸã ã—ミュートã—ã¦ã„るユーザーã‹ã‚‰ã®é€šçŸ¥ãªã‚‰ç„¡è¦– + const mutings = await this.mutingsRepository.findBy({ + muterId: notifieeId, + }); + if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) { + return; + } + //#endregion + + this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); + this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); + + if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); + if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); + }, () => { /* aborted, ignore it */ }); + + return notification; + } + + // TODO + //const locales = await import('../../../../locales/index.js'); + + // TODO: locale ファイルをクライアント用ã¨ã‚µãƒ¼ãƒãƒ¼ç”¨ã§åˆ†ã‘ãŸã„ + + @bindThis + private async emailNotificationFollow(userId: User['id'], follower: User) { + /* + const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); + if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; + const locale = locales[userProfile.lang ?? 'ja-JP']; + const i18n = new I18n(locale); + // TODO: render user information html + sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); + */ + } + + @bindThis + private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { + /* + const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); + if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; + const locale = locales[userProfile.lang ?? 'ja-JP']; + const i18n = new I18n(locale); + // TODO: render user information html + sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); + */ + } + + onApplicationShutdown(signal?: string | undefined): void { + this.#shutdownController.abort(); + } } diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 2cad1bc07e..32c38ad480 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import push from 'web-push'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { Packed } from '@/misc/schema'; +import type { Packed } from '@/misc/json-schema'; import { getNoteSummary } from '@/misc/get-note-summary.js'; import type { SwSubscriptionsRepository } from '@/models/index.js'; import { MetaService } from '@/core/MetaService.js'; diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index c334d749e6..0cee2076bf 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets, ObjectLiteral } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { User } from '@/models/entities/User.js'; -import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.js'; +import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import type { SelectQueryBuilder } from 'typeorm'; @@ -29,6 +29,9 @@ export class QueryService { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, + + @Inject(DI.renoteMutingsRepository) + private renoteMutingsRepository: RenoteMutingsRepository, ) { } @@ -269,5 +272,24 @@ export class QueryService { q.setParameters({ meId: me.id }); } } -} + @bindThis + public generateMutedUserRenotesQueryForNotes(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void { + const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting') + .select('renote_muting.muteeId') + .where('renote_muting.muterId = :muterId', { muterId: me.id }); + + q.andWhere(new Brackets(qb => { + qb + .where(new Brackets(qb => { + qb.where('note.renoteId IS NOT NULL'); + qb.andWhere('note.text IS NULL'); + qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`); + })) + .orWhere('note.renoteId IS NULL') + .orWhere('note.text IS NOT NULL'); + })); + + q.setParameters(mutingQuery.getParameters()); + } +} diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 4bf41e0ac1..498ceced7a 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -26,7 +26,7 @@ export class QueueService { ) {} @bindThis - public deliver(user: ThinUser, content: IActivity | null, to: string | null) { + public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) { if (content == null) return null; if (to == null) return null; @@ -36,6 +36,7 @@ export class QueueService { }, content, to, + isSharedInbox, }; return this.deliverQueue.add(data, { diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 9fccc14ee4..271ba79176 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -9,7 +9,7 @@ import { IdService } from '@/core/IdService.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; import { emojiRegex } from '@/misc/emoji-regex.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; @@ -79,7 +79,7 @@ export class ReactionService { private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, private perUserReactionsChart: PerUserReactionsChart, ) { } @@ -93,15 +93,19 @@ export class ReactionService { throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); } } - + // check visibility if (!await this.noteEntityService.isVisibleForMe(note, user.id)) { throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); } - - // TODO: cache - reaction = await this.toDbReaction(reaction, user.host); - + + if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) { + reaction = 'â¤ï¸'; + } else { + // TODO: cache + reaction = await this.toDbReaction(reaction, user.host); + } + const record: NoteReaction = { id: this.idService.genId(), createdAt: new Date(), @@ -109,7 +113,7 @@ export class ReactionService { userId: user.id, reaction, }; - + // Create reaction try { await this.noteReactionsRepository.insert(record); @@ -119,7 +123,7 @@ export class ReactionService { noteId: note.id, userId: user.id, }); - + if (exists.reaction !== reaction) { // 別ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ãŒã™ã§ã«ã•れã¦ã„ãŸã‚‰ç½®ãæ›ãˆã‚‹ await this.delete(user, note); @@ -132,7 +136,7 @@ export class ReactionService { throw e; } } - + // Increment reactions count const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; await this.notesRepository.createQueryBuilder().update() @@ -142,12 +146,12 @@ export class ReactionService { }) .where('id = :id', { id: note.id }) .execute(); - + this.perUserReactionsChart.update(user, note); - + // カスタム絵文å—リアクションã ã£ãŸã‚‰çµµæ–‡å—æƒ…å ±ã‚‚é€ã‚‹ const decodedReaction = this.decodeReaction(reaction); - + const emoji = await this.emojisRepository.findOne({ where: { name: decodedReaction.name, @@ -155,7 +159,7 @@ export class ReactionService { }, select: ['name', 'host', 'originalUrl', 'publicUrl'], }); - + this.globalEventService.publishNoteStream(note.id, 'reacted', { reaction: decodedReaction.reaction, emoji: emoji != null ? { @@ -165,16 +169,16 @@ export class ReactionService { } : null, userId: user.id, }); - + // リアクションã•れãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰é€šçŸ¥ã‚’ä½œæˆ if (note.userHost === null) { - this.createNotificationService.createNotification(note.userId, 'reaction', { + this.notificationService.createNotification(note.userId, 'reaction', { notifierId: user.id, noteId: note.id, reaction: reaction, }); } - + //#region é…ä¿¡ if (this.userEntityService.isLocalUser(user) && !note.localOnly) { const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note)); @@ -183,7 +187,7 @@ export class ReactionService { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); dm.addDirectRecipe(reactee as RemoteUser); } - + if (['public', 'home', 'followers'].includes(note.visibility)) { dm.addFollowersRecipe(); } else if (note.visibility === 'specified') { @@ -192,7 +196,7 @@ export class ReactionService { dm.addDirectRecipe(u as RemoteUser); } } - + dm.execute(); } //#endregion @@ -205,18 +209,18 @@ export class ReactionService { noteId: note.id, userId: user.id, }); - + if (exist == null) { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } - + // Delete reaction const result = await this.noteReactionsRepository.delete(exist.id); - + if (result.affected !== 1) { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } - + // Decrement reactions count const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; await this.notesRepository.createQueryBuilder().update() @@ -225,14 +229,14 @@ export class ReactionService { }) .where('id = :id', { id: note.id }) .execute(); - + if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1); - + this.globalEventService.publishNoteStream(note.id, 'unreacted', { reaction: this.decodeReaction(exist.reaction).reaction, userId: user.id, }); - + //#region é…ä¿¡ if (this.userEntityService.isLocalUser(user) && !note.localOnly) { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user)); @@ -246,7 +250,7 @@ export class ReactionService { } //#endregion } - + @bindThis public async getFallbackReaction(): Promise<string> { const meta = await this.metaService.fetch(); @@ -296,7 +300,7 @@ export class ReactionService { // Unicodeçµµæ–‡å— const match = emojiRegex.exec(reaction); if (match) { - // åˆå—ã‚’å«ã‚€1ã¤ã®çµµæ–‡å— + // åˆå—ã‚’å«ã‚€1ã¤ã®çµµæ–‡å— const unicode = match[0]; // 異体å—セレクタ除去 diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 2e07825e9b..86f983cc78 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -57,7 +57,7 @@ export class RelayService { const relayActor = await this.getRelayActor(); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); const activity = this.apRendererService.addContext(follow); - this.queueService.deliver(relayActor, activity, relay.inbox); + this.queueService.deliver(relayActor, activity, relay.inbox, false); return relay; } @@ -76,7 +76,7 @@ export class RelayService { const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor); const activity = this.apRendererService.addContext(undo); - this.queueService.deliver(relayActor, activity, relay.inbox); + this.queueService.deliver(relayActor, activity, relay.inbox, false); await this.relaysRepository.delete(relay.id); } @@ -120,7 +120,7 @@ export class RelayService { const signed = await this.apRendererService.attachLdSignature(copy, user); for (const relay of relays) { - this.queueService.deliver(user, signed, relay.inbox); + this.queueService.deliver(user, signed, relay.inbox, false); } } } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 7149591198..4775196c6f 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -21,6 +21,7 @@ export type RolePolicies = { canPublicNote: boolean; canInvite: boolean; canManageCustomEmojis: boolean; + canSearchNotes: boolean; canHideAds: boolean; driveCapacityMb: number; pinLimit: number; @@ -40,6 +41,7 @@ export const DEFAULT_POLICIES: RolePolicies = { canPublicNote: true, canInvite: false, canManageCustomEmojis: false, + canSearchNotes: false, canHideAds: false, driveCapacityMb: 100, pinLimit: 5, @@ -264,6 +266,7 @@ export class RoleService implements OnApplicationShutdown { canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), + canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), pinLimit: calc('pinLimit', vs => Math.max(...vs)), diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index 0ce69aaa74..cc8f950813 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -19,12 +19,14 @@ export class S3Service { @bindThis public getS3(meta: Meta) { - const u = meta.objectStorageEndpoint != null + const u = meta.objectStorageEndpoint ? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}` : `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`; - + return new S3({ - endpoint: meta.objectStorageEndpoint ?? undefined, + endpoint: meta.objectStorageEndpoint && meta.objectStorageEndpoint.length > 0 + ? meta.objectStorageEndpoint + : undefined, accessKeyId: meta.objectStorageAccessKey!, secretAccessKey: meta.objectStorageSecretKey!, region: meta.objectStorageRegion ?? undefined, diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 90a7186909..d7bc05b8bd 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -90,7 +90,7 @@ export class SignupService { cipher: undefined, passphrase: undefined, }, - } as any, (err, publicKey, privateKey) => + }, (err, publicKey, privateKey) => err ? rej(err) : res([publicKey, privateKey]), )); diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index be37bad52e..92408da342 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -118,7 +118,7 @@ export class UserBlockingService implements OnApplicationShutdown { if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { const content = this.apRendererService.addContext(this.apRendererService.renderBlock(blocking)); - this.queueService.deliver(blocker, content, blockee.inbox); + this.queueService.deliver(blocker, content, blockee.inbox, false); } } @@ -163,13 +163,13 @@ export class UserBlockingService implements OnApplicationShutdown { // リモートã«ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ã—ã¦ã„ãŸã‚‰UndoFollowé€ä¿¡ if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); - this.queueService.deliver(follower, content, followee.inbox); + this.queueService.deliver(follower, content, followee.inbox, false); } // リモートã‹ã‚‰ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’å—ã‘ã¦ã„ãŸã‚‰Rejecté€ä¿¡ if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); - this.queueService.deliver(followee, content, follower.inbox); + this.queueService.deliver(followee, content, follower.inbox, false); } } @@ -211,13 +211,13 @@ export class UserBlockingService implements OnApplicationShutdown { // リモートã«ãƒ•ã‚©ãƒãƒ¼ã‚’ã—ã¦ã„ãŸã‚‰UndoFollowé€ä¿¡ if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); - this.queueService.deliver(follower, content, followee.inbox); + this.queueService.deliver(follower, content, followee.inbox, false); } // リモートã‹ã‚‰ãƒ•ã‚©ãƒãƒ¼ã‚’ã•れã¦ã„ãŸã‚‰RejectFollowé€ä¿¡ if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); - this.queueService.deliver(followee, content, follower.inbox); + this.queueService.deliver(followee, content, follower.inbox, false); } } @@ -262,7 +262,7 @@ export class UserBlockingService implements OnApplicationShutdown { // deliver if remote bloking if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); - this.queueService.deliver(blocker, content, blockee.inbox); + this.queueService.deliver(blocker, content, blockee.inbox, false); } } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index d8426512bf..1c85504353 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -6,11 +6,11 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { WebhookService } from '@/core/WebhookService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -57,7 +57,7 @@ export class UserFollowingService { private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, private webhookService: WebhookService, private apRendererService: ApRendererService, @@ -82,7 +82,7 @@ export class UserFollowingService { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { // リモートフォãƒãƒ¼ã‚’å—ã‘ã¦ãƒ–ãƒãƒƒã‚¯ã—ã¦ã„ãŸå ´åˆã¯ã€ã‚¨ãƒ©ãƒ¼ã«ã™ã‚‹ã®ã§ã¯ãªãRejectã‚’é€ã‚Šè¿”ã—ã¦ãŠã—ã¾ã„。 const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); - this.queueService.deliver(followee, content, follower.inbox); + this.queueService.deliver(followee, content, follower.inbox, false); return; } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { // リモートフォãƒãƒ¼ã‚’å—ã‘ã¦ãƒ–ãƒãƒƒã‚¯ã•れã¦ã„ã‚‹ã¯ãšã®å ´åˆã ã£ãŸã‚‰ã€ãƒ–ãƒãƒƒã‚¯è§£é™¤ã—ã¦ãŠã。 @@ -131,7 +131,7 @@ export class UserFollowingService { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); - this.queueService.deliver(followee, content, follower.inbox); + this.queueService.deliver(followee, content, follower.inbox, false); } } @@ -145,15 +145,15 @@ export class UserFollowingService { }, ): Promise<void> { if (follower.id === followee.id) return; - + let alreadyFollowed = false as boolean; - + await this.followingsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), followerId: follower.id, followeeId: followee.id, - + // éžæ£è¦åŒ– followerHost: follower.host, followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null, @@ -169,35 +169,35 @@ export class UserFollowingService { throw err; } }); - + const req = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, followerId: follower.id, }); - + if (req) { await this.followRequestsRepository.delete({ followeeId: followee.id, followerId: follower.id, }); - + // é€šçŸ¥ã‚’ä½œæˆ - this.createNotificationService.createNotification(follower.id, 'followRequestAccepted', { + this.notificationService.createNotification(follower.id, 'followRequestAccepted', { notifierId: followee.id, }); } - + if (alreadyFollowed) return; this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); - + //#region Increment counts await Promise.all([ this.usersRepository.increment({ id: follower.id }, 'followingCount', 1), this.usersRepository.increment({ id: followee.id }, 'followersCount', 1), ]); //#endregion - + //#region Update instance stats if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(i => { @@ -211,9 +211,9 @@ export class UserFollowingService { }); } //#endregion - + this.perUserFollowingChart.update(follower, followee, true); - + // Publish follow event if (this.userEntityService.isLocalUser(follower)) { this.userEntityService.pack(followee.id, follower, { @@ -221,7 +221,7 @@ export class UserFollowingService { }).then(async packed => { this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); - + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); for (const webhook of webhooks) { this.queueService.webhookDeliver(webhook, 'follow', { @@ -230,12 +230,12 @@ export class UserFollowingService { } }); } - + // Publish followed event if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(async packed => { this.globalEventService.publishMainStream(followee.id, 'followed', packed); - + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); for (const webhook of webhooks) { this.queueService.webhookDeliver(webhook, 'followed', { @@ -243,9 +243,9 @@ export class UserFollowingService { }); } }); - + // é€šçŸ¥ã‚’ä½œæˆ - this.createNotificationService.createNotification(followee.id, 'follow', { + this.notificationService.createNotification(followee.id, 'follow', { notifierId: follower.id, }); } @@ -265,16 +265,16 @@ export class UserFollowingService { followerId: follower.id, followeeId: followee.id, }); - + if (following == null) { logger.warn('フォãƒãƒ¼è§£é™¤ãŒãƒªã‚¯ã‚¨ã‚¹ãƒˆã•れã¾ã—ãŸãŒãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ã¾ã›ã‚“ã§ã—ãŸ'); return; } - + await this.followingsRepository.delete(following.id); - + this.decrementFollowing(follower, followee); - + // Publish unfollow event if (!silent && this.userEntityService.isLocalUser(follower)) { this.userEntityService.pack(followee.id, follower, { @@ -282,7 +282,7 @@ export class UserFollowingService { }).then(async packed => { this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); - + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { this.queueService.webhookDeliver(webhook, 'unfollow', { @@ -291,33 +291,33 @@ export class UserFollowingService { } }); } - + if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); - this.queueService.deliver(follower, content, followee.inbox); + this.queueService.deliver(follower, content, followee.inbox, false); } - + if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { // local user has null host const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); - this.queueService.deliver(followee, content, follower.inbox); + this.queueService.deliver(followee, content, follower.inbox, false); } } - + @bindThis private async decrementFollowing( - follower: {id: User['id']; host: User['host']; }, + follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, ): Promise<void> { this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id }); - + //#region Decrement following / followers counts await Promise.all([ this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1), ]); //#endregion - + //#region Update instance stats if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(i => { @@ -331,7 +331,7 @@ export class UserFollowingService { }); } //#endregion - + this.perUserFollowingChart.update(follower, followee, false); } @@ -346,23 +346,23 @@ export class UserFollowingService { requestId?: string, ): Promise<void> { if (follower.id === followee.id) return; - + // check blocking const [blocking, blocked] = await Promise.all([ this.userBlockingService.checkBlocked(follower.id, followee.id), this.userBlockingService.checkBlocked(followee.id, follower.id), ]); - + if (blocking) throw new Error('blocking'); if (blocked) throw new Error('blocked'); - + const followRequest = await this.followRequestsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), followerId: follower.id, followeeId: followee.id, requestId, - + // éžæ£è¦åŒ– followerHost: follower.host, followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined, @@ -371,25 +371,25 @@ export class UserFollowingService { followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined, followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined, }).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0])); - + // Publish receiveRequest event if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed)); - + this.userEntityService.pack(followee.id, followee, { detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); - + // é€šçŸ¥ã‚’ä½œæˆ - this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', { + this.notificationService.createNotification(followee.id, 'receiveFollowRequest', { notifierId: follower.id, followRequestId: followRequest.id, }); } - + if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)); - this.queueService.deliver(follower, content, followee.inbox); + this.queueService.deliver(follower, content, followee.inbox, false); } } @@ -404,26 +404,26 @@ export class UserFollowingService { ): Promise<void> { if (this.userEntityService.isRemoteUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); - + if (this.userEntityService.isLocalUser(follower)) { // 本æ¥ã“ã®ãƒã‚§ãƒƒã‚¯ã¯ä¸è¦ã ã‘ã©TSã«æ€’られるã®ã§ - this.queueService.deliver(follower, content, followee.inbox); + this.queueService.deliver(follower, content, followee.inbox, false); } } - + const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, followerId: follower.id, }); - + if (request == null) { throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); } - + await this.followRequestsRepository.delete({ followeeId: followee.id, followerId: follower.id, }); - + this.userEntityService.pack(followee.id, followee, { detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); @@ -440,18 +440,18 @@ export class UserFollowingService { followeeId: followee.id, followerId: follower.id, }); - + if (request == null) { throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.'); } - + await this.insertFollowingDoc(followee, follower); - + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); - this.queueService.deliver(followee, content, follower.inbox); + this.queueService.deliver(followee, content, follower.inbox, false); } - + this.userEntityService.pack(followee.id, followee, { detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); @@ -466,13 +466,13 @@ export class UserFollowingService { const requests = await this.followRequestsRepository.findBy({ followeeId: user.id, }); - + for (const request of requests) { const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId }); this.acceptFollowRequest(user, follower); } } - + /** * API following/request/reject */ @@ -557,7 +557,7 @@ export class UserFollowingService { }); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee)); - this.queueService.deliver(followee, content, follower.inbox); + this.queueService.deliver(followee, content, follower.inbox, false); } /** diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 02903a0590..d00bb89c76 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -54,7 +54,7 @@ export class UserSuspendService { } for (const inbox of queue) { - this.queueService.deliver(user, content, inbox); + this.queueService.deliver(user, content, inbox, true); } } } @@ -84,7 +84,7 @@ export class UserSuspendService { } for (const inbox of queue) { - this.queueService.deliver(user as any, content, inbox); + this.queueService.deliver(user as any, content, inbox, true); } } } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 5e6ea69846..70a6d32fe2 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -157,7 +157,8 @@ class DeliverManager { public async execute() { if (!this.userEntityService.isLocalUser(this.actor)) return; - const inboxes = new Set<string>(); + // The value flags whether it is shared or not. + const inboxes = new Map<string, boolean>(); /* build inbox list @@ -185,7 +186,7 @@ class DeliverManager { for (const following of followers) { const inbox = following.followerSharedInbox ?? following.followerInbox; - inboxes.add(inbox); + inboxes.set(inbox, following.followerSharedInbox === null); } } @@ -197,11 +198,12 @@ class DeliverManager { // check that they actually have an inbox && recipe.to.inbox != null, ) - .forEach(recipe => inboxes.add(recipe.to.inbox!)); + .forEach(recipe => inboxes.set(recipe.to.inbox!, false)); // deliver for (const inbox of inboxes) { - this.queueService.deliver(this.actor, this.activity, inbox); + // inbox[0]: inbox, inbox[1]: whether it is sharedInbox + this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]); } } } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 6d9569bce2..055bffe731 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -140,7 +140,7 @@ export class ApInboxService { } else if (isFlag(activity)) { await this.flag(actor, activity); } else { - this.logger.warn(`unrecognized activity type: ${(activity as any).type}`); + this.logger.warn(`unrecognized activity type: ${activity.type}`); } } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 6a1f233bd8..0d03e3d904 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -91,6 +91,9 @@ export class ApRendererService { } else if (note.visibility === 'home') { to = [`${attributedTo}/followers`]; cc = ['https://www.w3.org/ns/activitystreams#Public']; + } else if (note.visibility === 'followers') { + to = [`${attributedTo}/followers`]; + cc = []; } else { throw new Error('renderAnnounce: cannot render non-public note'); } @@ -116,7 +119,7 @@ export class ApRendererService { if (block.blockee?.uri == null) { throw new Error('renderBlock: missing blockee uri'); } - + return { type: 'Block', id: `${this.config.url}/blocks/${block.id}`, @@ -134,10 +137,10 @@ export class ApRendererService { published: note.createdAt.toISOString(), object, } as ICreate; - + if (object.to) activity.to = object.to; if (object.cc) activity.cc = object.cc; - + return activity; } @@ -155,7 +158,7 @@ export class ApRendererService { public renderDocument(file: DriveFile): IApDocument { return { type: 'Document', - mediaType: file.type, + mediaType: file.webpublicType ?? file.type, url: this.driveFileEntityService.getPublicUrl(file), name: file.comment, }; @@ -297,16 +300,16 @@ export class ApRendererService { const items = await this.driveFilesRepository.findBy({ id: In(ids) }); return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; }; - + let inReplyTo; let inReplyToNote: Note | null; - + if (note.replyId) { inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); - + if (inReplyToNote != null) { const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); - + if (inReplyToUser != null) { if (inReplyToNote.uri) { inReplyTo = inReplyToNote.uri; @@ -322,24 +325,24 @@ export class ApRendererService { } else { inReplyTo = null; } - + let quote; - + if (note.renoteId) { const renote = await this.notesRepository.findOneBy({ id: note.renoteId }); - + if (renote) { quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`; } } - + const attributedTo = `${this.config.url}/users/${note.userId}`; - + const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); - + let to: string[] = []; let cc: string[] = []; - + if (note.visibility === 'public') { to = ['https://www.w3.org/ns/activitystreams#Public']; cc = [`${attributedTo}/followers`].concat(mentions); @@ -352,44 +355,44 @@ export class ApRendererService { } else { to = mentions; } - + const mentionedUsers = note.mentions.length > 0 ? await this.usersRepository.findBy({ id: In(note.mentions), }) : []; - + const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag)); const mentionTags = mentionedUsers.map(u => this.renderMention(u)); - + const files = await getPromisedFiles(note.fileIds); - + const text = note.text ?? ''; let poll: Poll | null = null; - + if (note.hasPoll) { poll = await this.pollsRepository.findOneBy({ noteId: note.id }); } - + let apText = text; - + if (quote) { apText += `\n\nRE: ${quote}`; } - + const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - + const content = this.apMfmService.getNoteHtml(Object.assign({}, note, { text: apText, })); - + const emojis = await this.getEmojis(note.emojis); const apemojis = emojis.map(emoji => this.renderEmoji(emoji)); - + const tag = [ ...hashtagTags, ...mentionTags, ...apemojis, ]; - + const asPoll = poll ? { type: 'Question', content: this.apMfmService.getNoteHtml(Object.assign({}, note, { @@ -601,7 +604,7 @@ export class ApRendererService { if (typeof x === 'object' && x.id == null) { x.id = `${this.config.url}/${uuid()}`; } - + return Object.assign({ '@context': [ 'https://www.w3.org/ns/activitystreams', @@ -634,18 +637,18 @@ export class ApRendererService { ], }, x as T & { id: string; }); } - + @bindThis public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> { const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); - + const ldSignature = this.ldSignatureService.use(); ldSignature.debug = false; activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); - + return activity; } - + /** * Render OrderedCollectionPage * @param id URL of self @@ -686,11 +689,11 @@ export class ApRendererService { type: 'OrderedCollection', totalItems, }; - + if (first) page.first = first; if (last) page.last = last; if (orderedItems) page.orderedItems = orderedItems; - + return page; } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index c36e8d4ed6..28bcbc6fab 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -124,7 +124,7 @@ export class ApNoteService { throw new Error('invalid note'); } - const note: IPost = object as any; + const note = object as IPost; this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); @@ -180,7 +180,7 @@ export class ApNoteService { const reply: Note | null = note.inReplyTo ? await this.resolveNote(note.inReplyTo, resolver).then(x => { if (x == null) { - this.logger.warn('Specified inReplyTo, but nout found'); + this.logger.warn('Specified inReplyTo, but not found'); throw new Error('inReplyTo not found'); } else { return x; diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index a1fdd7a198..d06958da0c 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -164,6 +164,9 @@ export class ApPersonService implements OnModuleInit { throw new Error('invalid Actor: wrong name'); } x.name = truncate(x.name, nameLength); + } else if (x.name === '') { + // Mastodon emits empty string when the name is not set. + x.name = undefined; } if (x.summary) { if (!(typeof x.summary === 'string' && x.summary.length > 0)) { diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 7f2ca9c05e..8851946330 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -195,7 +195,8 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue => object && getApType(object) === 'PropertyValue' && typeof object.name === 'string' && - typeof (object as any).value === 'string'; + 'value' in object && + typeof object.value === 'string'; export interface IApMention extends IObject { type: 'Mention'; diff --git a/packages/backend/src/core/chart/charts/entities/active-users.ts b/packages/backend/src/core/chart/charts/entities/active-users.ts index 5767b76f8e..e291e37c1b 100644 --- a/packages/backend/src/core/chart/charts/entities/active-users.ts +++ b/packages/backend/src/core/chart/charts/entities/active-users.ts @@ -3,15 +3,15 @@ import Chart from '../../core.js'; export const name = 'activeUsers'; export const schema = { - 'readWrite': { intersection: ['read', 'write'], range: 'small' }, - 'read': { uniqueIncrement: true, range: 'small' }, - 'write': { uniqueIncrement: true, range: 'small' }, - 'registeredWithinWeek': { uniqueIncrement: true, range: 'small' }, - 'registeredWithinMonth': { uniqueIncrement: true, range: 'small' }, - 'registeredWithinYear': { uniqueIncrement: true, range: 'small' }, - 'registeredOutsideWeek': { uniqueIncrement: true, range: 'small' }, - 'registeredOutsideMonth': { uniqueIncrement: true, range: 'small' }, - 'registeredOutsideYear': { uniqueIncrement: true, range: 'small' }, + 'readWrite': { intersection: ['read', 'write'] }, + 'read': { uniqueIncrement: true }, + 'write': { uniqueIncrement: true }, + 'registeredWithinWeek': { uniqueIncrement: true }, + 'registeredWithinMonth': { uniqueIncrement: true }, + 'registeredWithinYear': { uniqueIncrement: true }, + 'registeredOutsideWeek': { uniqueIncrement: true }, + 'registeredOutsideMonth': { uniqueIncrement: true }, + 'registeredOutsideYear': { uniqueIncrement: true }, } as const; export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index c8a98c0bea..e02daefd64 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { AntennaNotesRepository, AntennasRepository } from '@/models/index.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { Antenna } from '@/models/entities/Antenna.js'; import { bindThis } from '@/decorators.js'; @@ -37,6 +37,7 @@ export class AntennaEntityService { notify: antenna.notify, withReplies: antenna.withReplies, withFile: antenna.withFile, + isActive: antenna.isActive, hasUnreadNote, }; } diff --git a/packages/backend/src/core/entities/AppEntityService.ts b/packages/backend/src/core/entities/AppEntityService.ts index 36cd48f3ce..0b4c3935c7 100644 --- a/packages/backend/src/core/entities/AppEntityService.ts +++ b/packages/backend/src/core/entities/AppEntityService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, AppsRepository } from '@/models/index.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { App } from '@/models/entities/App.js'; import type { User } from '@/models/entities/User.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts index c9e15207b9..e169c7e90a 100644 --- a/packages/backend/src/core/entities/BlockingEntityService.ts +++ b/packages/backend/src/core/entities/BlockingEntityService.ts @@ -2,11 +2,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { BlockingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { Blocking } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; -import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class BlockingEntityService { diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 0a9bcf85c4..6048492f09 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -1,13 +1,13 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Channel } from '@/models/entities/Channel.js'; +import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ChannelEntityService { diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts index 63c50865e0..33d3c53806 100644 --- a/packages/backend/src/core/entities/ClipEntityService.ts +++ b/packages/backend/src/core/entities/ClipEntityService.ts @@ -1,12 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ClipsRepository } from '@/models/index.js'; +import type { ClipFavoritesRepository, ClipsRepository, User } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { Clip } from '@/models/entities/Clip.js'; -import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class ClipEntityService { @@ -14,6 +14,9 @@ export class ClipEntityService { @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, + @Inject(DI.clipFavoritesRepository) + private clipFavoritesRepository: ClipFavoritesRepository, + private userEntityService: UserEntityService, ) { } @@ -21,25 +24,31 @@ export class ClipEntityService { @bindThis public async pack( src: Clip['id'] | Clip, + me?: { id: User['id'] } | null | undefined, ): Promise<Packed<'Clip'>> { + const meId = me ? me.id : null; const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src }); return await awaitAll({ id: clip.id, createdAt: clip.createdAt.toISOString(), + lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null, userId: clip.userId, user: this.userEntityService.pack(clip.user ?? clip.userId), name: clip.name, description: clip.description, isPublic: clip.isPublic, + favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }), + isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined, }); } @bindThis public packMany( clips: Clip[], + me?: { id: User['id'] } | null | undefined, ) { - return Promise.all(clips.map(x => this.pack(x))); + return Promise.all(clips.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index f769ddd5e9..2d40f444cb 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -3,7 +3,7 @@ import { DataSource, In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { User } from '@/models/entities/User.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; @@ -89,9 +89,7 @@ export class DriveFileEntityService { if (file.type.startsWith('video')) { if (file.thumbnailUrl) return file.thumbnailUrl; - if (this.config.videoThumbnailGenerator == null) { - return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri); - } + return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri); } else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { // å‹•ç”»ã§ã¯ãªãリモートã‹ã¤ãƒ¡ãƒ‡ã‚£ã‚¢ãƒ—ãƒã‚ã‚· return this.getProxiedUrl(file.uri, 'static'); @@ -106,7 +104,7 @@ export class DriveFileEntityService { const url = file.webpublicUrl ?? file.url; - return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? this.getProxiedUrl(url, 'static') : null); + return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? url : null); } @bindThis diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts index 93c52c91f2..13929b145f 100644 --- a/packages/backend/src/core/entities/DriveFolderEntityService.ts +++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { DriveFolder } from '@/models/entities/DriveFolder.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index f5c8f2d4bb..3bad048bc0 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { EmojisRepository } from '@/models/index.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import { bindThis } from '@/decorators.js'; @@ -50,6 +50,7 @@ export class EmojiEntityService { host: emoji.host, // || emoji.originalUrl ã—ã¦ã‚‹ã®ã¯å¾Œæ–¹äº’æ›æ€§ã®ãŸã‚(publicUrlã¯stringãªã®ã§??ã¯ã ã‚) url: emoji.publicUrl || emoji.originalUrl, + license: emoji.license, }; } diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts index 61bd18c04f..e52a591884 100644 --- a/packages/backend/src/core/entities/FlashEntityService.ts +++ b/packages/backend/src/core/entities/FlashEntityService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Flash } from '@/models/entities/Flash.js'; diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts index a833ae719b..55ba4e67ad 100644 --- a/packages/backend/src/core/entities/FollowingEntityService.ts +++ b/packages/backend/src/core/entities/FollowingEntityService.ts @@ -2,10 +2,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Following } from '@/models/entities/Following.js'; +import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; type LocalFollowerFollowing = Following & { @@ -31,7 +32,6 @@ type RemoteFolloweeFollowing = Following & { followeeInbox: string; followeeSharedInbox: string; }; -import { bindThis } from '@/decorators.js'; @Injectable() export class FollowingEntityService { diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts index fb147ae181..632c75304f 100644 --- a/packages/backend/src/core/entities/GalleryPostEntityService.ts +++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts @@ -2,13 +2,13 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { GalleryPost } from '@/models/entities/GalleryPost.js'; +import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class GalleryPostEntityService { diff --git a/packages/backend/src/core/entities/HashtagEntityService.ts b/packages/backend/src/core/entities/HashtagEntityService.ts index 88b1ff3a38..2cd79b8f8c 100644 --- a/packages/backend/src/core/entities/HashtagEntityService.ts +++ b/packages/backend/src/core/entities/HashtagEntityService.ts @@ -1,11 +1,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { HashtagsRepository } from '@/models/index.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { Hashtag } from '@/models/entities/Hashtag.js'; -import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class HashtagEntityService { diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 8a9706816b..3bf84ed375 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -1,12 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { InstancesRepository } from '@/models/index.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { Instance } from '@/models/entities/Instance.js'; import { MetaService } from '@/core/MetaService.js'; -import { UtilityService } from '../UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { UtilityService } from '../UtilityService.js'; @Injectable() export class InstanceEntityService { diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts index 4f02ef4087..561d53292e 100644 --- a/packages/backend/src/core/entities/MutingEntityService.ts +++ b/packages/backend/src/core/entities/MutingEntityService.ts @@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { MutingsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Muting } from '@/models/entities/Muting.js'; -import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class MutingEntityService { diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 4ec10df9a6..5660600692 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -3,7 +3,7 @@ import { DataSource, In } from 'typeorm'; import * as mfm from 'mfm-js'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { nyaize } from '@/misc/nyaize.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { User } from '@/models/entities/User.js'; @@ -314,6 +314,7 @@ export class NoteEntityService implements OnModuleInit { cw: note.cw, visibility: note.visibility, localOnly: note.localOnly ?? undefined, + reactionAcceptance: note.reactionAcceptance, visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, renoteCount: note.renoteCount, repliesCount: note.repliesCount, diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts index 9779489673..8f943ba24c 100644 --- a/packages/backend/src/core/entities/NoteReactionEntityService.ts +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -1,7 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { NoteReactionsRepository } from '@/models/index.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { bindThis } from '@/decorators.js'; import type { OnModuleInit } from '@nestjs/common'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; @@ -10,7 +11,6 @@ import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; import { ModuleRef } from '@nestjs/core'; -import { bindThis } from '@/decorators.js'; @Injectable() export class NoteReactionEntityService implements OnModuleInit { diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index be88a213f4..70e56cb3d7 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -5,7 +5,7 @@ import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepo import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Notification } from '@/models/entities/Notification.js'; import type { Note } from '@/models/entities/Note.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { notificationTypes } from '@/types.js'; diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts index 48e45dd019..d6da856637 100644 --- a/packages/backend/src/core/entities/PageEntityService.ts +++ b/packages/backend/src/core/entities/PageEntityService.ts @@ -2,14 +2,14 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { Page } from '@/models/entities/Page.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; +import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class PageEntityService { diff --git a/packages/backend/src/core/entities/RenoteMutingEntityService.ts b/packages/backend/src/core/entities/RenoteMutingEntityService.ts new file mode 100644 index 0000000000..f8871e0495 --- /dev/null +++ b/packages/backend/src/core/entities/RenoteMutingEntityService.ts @@ -0,0 +1,47 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { RenoteMutingsRepository } from '@/models/index.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { } from '@/models/entities/Blocking.js'; +import type { User } from '@/models/entities/User.js'; +import type { RenoteMuting } from '@/models/entities/RenoteMuting.js'; +import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class RenoteMutingEntityService { + constructor( + @Inject(DI.renoteMutingsRepository) + private renoteMutingsRepository: RenoteMutingsRepository, + + private userEntityService: UserEntityService, + ) { + } + + @bindThis + public async pack( + src: RenoteMuting['id'] | RenoteMuting, + me?: { id: User['id'] } | null | undefined, + ): Promise<Packed<'RenoteMuting'>> { + const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: muting.id, + createdAt: muting.createdAt.toISOString(), + muteeId: muting.muteeId, + mutee: this.userEntityService.pack(muting.muteeId, me, { + detail: true, + }), + }); + } + + @bindThis + public packMany( + mutings: any[], + me: { id: User['id'] }, + ) { + return Promise.all(mutings.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 2f1d51fa1a..e111a10b77 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -61,6 +61,7 @@ export class RoleEntityService { isModerator: role.isModerator, asBadge: role.asBadge, canEditMembersByModerator: role.canEditMembersByModerator, + displayOrder: role.displayOrder, policies: policies, usersCount: assignedCount, }); diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 3635643218..068ffad09d 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -4,7 +4,7 @@ import Ajv from 'ajv'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; @@ -12,7 +12,7 @@ import { Cache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; -import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js'; +import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -78,6 +78,9 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, + @Inject(DI.renoteMutingsRepository) + private renoteMutingsRepository: RenoteMutingsRepository, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -195,6 +198,13 @@ export class UserEntityService implements OnModuleInit { }, take: 1, }).then(n => n > 0), + isRenoteMuted: this.renoteMutingsRepository.count({ + where: { + muterId: me, + muteeId: target, + }, + take: 1, + }).then(n => n > 0), }); } @@ -380,9 +390,10 @@ export class UserEntityService implements OnModuleInit { emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), // パフォーマンス上ã®ç†ç”±ã§ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ - badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.map(r => ({ + badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({ name: r.name, iconUrl: r.iconUrl, + displayOrder: r.displayOrder, }))) : undefined, ...(opts.detail ? { @@ -419,7 +430,7 @@ export class UserEntityService implements OnModuleInit { userId: user.id, }).then(result => result >= 1) : false, - roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).map(role => ({ + roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ id: role.id, name: role.name, color: role.color, @@ -427,6 +438,7 @@ export class UserEntityService implements OnModuleInit { description: role.description, isModerator: role.isModerator, isAdministrator: role.isAdministrator, + displayOrder: role.displayOrder, }))), } : {}), @@ -493,6 +505,7 @@ export class UserEntityService implements OnModuleInit { isBlocking: relation.isBlocking, isBlocked: relation.isBlocked, isMuted: relation.isMuted, + isRenoteMuted: relation.isRenoteMuted, } : {}), } as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>; diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index 3c1087881a..2461cb2c12 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -1,11 +1,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { UserList } from '@/models/entities/UserList.js'; -import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class UserListEntityService { diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 05603093be..0879735b1d 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -36,6 +36,7 @@ export const DI = { notificationsRepository: Symbol('notificationsRepository'), metasRepository: Symbol('metasRepository'), mutingsRepository: Symbol('mutingsRepository'), + renoteMutingsRepository: Symbol('renoteMutingsRepository'), blockingsRepository: Symbol('blockingsRepository'), swSubscriptionsRepository: Symbol('swSubscriptionsRepository'), hashtagsRepository: Symbol('hashtagsRepository'), @@ -51,6 +52,7 @@ export const DI = { moderationLogsRepository: Symbol('moderationLogsRepository'), clipsRepository: Symbol('clipsRepository'), clipNotesRepository: Symbol('clipNotesRepository'), + clipFavoritesRepository: Symbol('clipFavoritesRepository'), antennasRepository: Symbol('antennasRepository'), antennaNotesRepository: Symbol('antennaNotesRepository'), promoNotesRepository: Symbol('promoNotesRepository'), diff --git a/packages/backend/src/misc/correct-filename.ts b/packages/backend/src/misc/correct-filename.ts index 3357d8c1bd..23a0699f39 100644 --- a/packages/backend/src/misc/correct-filename.ts +++ b/packages/backend/src/misc/correct-filename.ts @@ -1,15 +1,15 @@ // 与ãˆã‚‰ã‚ŒãŸæ‹¡å¼µåã¨ãƒ•ァイルåãŒä¸€è‡´ã—ã¦ã„ã‚‹ã‹ã©ã†ã‹ã‚’確èªã—〠// 一致ã—ã¦ã„ãªã„å ´åˆã¯æ‹¡å¼µåを付与ã—ã¦è¿”ã™ export function correctFilename(filename: string, ext: string | null) { - const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown'; - if (filename.endsWith(dotExt)) { - return filename; - } - if (ext === 'jpg' && filename.endsWith('.jpeg')) { - return filename; - } - if (ext === 'tif' && filename.endsWith('.tiff')) { - return filename; - } - return `${filename}${dotExt}`; + const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown'; + if (filename.endsWith(dotExt)) { + return filename; + } + if (ext === 'jpg' && filename.endsWith('.jpeg')) { + return filename; + } + if (ext === 'tif' && filename.endsWith('.tiff')) { + return filename; + } + return `${filename}${dotExt}`; } diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index 85bc2ec94d..964f20b25b 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -1,4 +1,4 @@ -import type { Packed } from './schema.js'; +import type { Packed } from './json-schema.js'; /** * æŠ•ç¨¿ã‚’è¡¨ã™æ–‡å—列をå–å¾—ã—ã¾ã™ã€‚ diff --git a/packages/backend/src/misc/is-instance-muted.ts b/packages/backend/src/misc/is-instance-muted.ts index e11a18bb70..73ad0b3b82 100644 --- a/packages/backend/src/misc/is-instance-muted.ts +++ b/packages/backend/src/misc/is-instance-muted.ts @@ -1,4 +1,4 @@ -import type { Packed } from './schema.js'; +import type { Packed } from './json-schema.js'; export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set<string>): boolean { if (mutedInstances.has(note.user.host ?? '')) return true; diff --git a/packages/backend/src/misc/is-mime-image.ts b/packages/backend/src/misc/is-mime-image.ts index 0b6d147dc1..46a66efc0f 100644 --- a/packages/backend/src/misc/is-mime-image.ts +++ b/packages/backend/src/misc/is-mime-image.ts @@ -2,10 +2,10 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; const dictionary = { 'safe-file': FILE_TYPE_BROWSERSAFE, - 'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'], - 'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'], - 'sharp-convertible-image-with-bmp': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'], - 'sharp-animation-convertible-image-with-bmp': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'], + 'sharp-convertible-image': ['image/jpeg', 'image/tiff', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'], + 'sharp-animation-convertible-image': ['image/jpeg', 'image/tiff', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'], + 'sharp-convertible-image-with-bmp': ['image/jpeg', 'image/tiff', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'], + 'sharp-animation-convertible-image-with-bmp': ['image/jpeg', 'image/tiff', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'], }; export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime); diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/json-schema.ts index 6a0802f8a4..e748f93a26 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -6,28 +6,29 @@ import { packedMeDetailedSchema, packedUserDetailedSchema, packedUserSchema, -} from '@/models/schema/user.js'; -import { packedNoteSchema } from '@/models/schema/note.js'; -import { packedUserListSchema } from '@/models/schema/user-list.js'; -import { packedAppSchema } from '@/models/schema/app.js'; -import { packedNotificationSchema } from '@/models/schema/notification.js'; -import { packedDriveFileSchema } from '@/models/schema/drive-file.js'; -import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js'; -import { packedFollowingSchema } from '@/models/schema/following.js'; -import { packedMutingSchema } from '@/models/schema/muting.js'; -import { packedBlockingSchema } from '@/models/schema/blocking.js'; -import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js'; -import { packedHashtagSchema } from '@/models/schema/hashtag.js'; -import { packedPageSchema } from '@/models/schema/page.js'; -import { packedNoteFavoriteSchema } from '@/models/schema/note-favorite.js'; -import { packedChannelSchema } from '@/models/schema/channel.js'; -import { packedAntennaSchema } from '@/models/schema/antenna.js'; -import { packedClipSchema } from '@/models/schema/clip.js'; -import { packedFederationInstanceSchema } from '@/models/schema/federation-instance.js'; -import { packedQueueCountSchema } from '@/models/schema/queue.js'; -import { packedGalleryPostSchema } from '@/models/schema/gallery-post.js'; -import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/schema/emoji.js'; -import { packedFlashSchema } from '@/models/schema/flash.js'; +} from '@/models/json-schema/user.js'; +import { packedNoteSchema } from '@/models/json-schema/note.js'; +import { packedUserListSchema } from '@/models/json-schema/user-list.js'; +import { packedAppSchema } from '@/models/json-schema/app.js'; +import { packedNotificationSchema } from '@/models/json-schema/notification.js'; +import { packedDriveFileSchema } from '@/models/json-schema/drive-file.js'; +import { packedDriveFolderSchema } from '@/models/json-schema/drive-folder.js'; +import { packedFollowingSchema } from '@/models/json-schema/following.js'; +import { packedMutingSchema } from '@/models/json-schema/muting.js'; +import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'; +import { packedBlockingSchema } from '@/models/json-schema/blocking.js'; +import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; +import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; +import { packedPageSchema } from '@/models/json-schema/page.js'; +import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js'; +import { packedChannelSchema } from '@/models/json-schema/channel.js'; +import { packedAntennaSchema } from '@/models/json-schema/antenna.js'; +import { packedClipSchema } from '@/models/json-schema/clip.js'; +import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js'; +import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; +import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; +import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; +import { packedFlashSchema } from '@/models/json-schema/flash.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -48,6 +49,7 @@ export const refs = { DriveFolder: packedDriveFolderSchema, Following: packedFollowingSchema, Muting: packedMutingSchema, + RenoteMuting: packedRenoteMutingSchema, Blocking: packedBlockingSchema, Hashtag: packedHashtagSchema, Page: packedPageSchema, @@ -93,7 +95,7 @@ export interface Schema extends OfSchema { readonly example?: any; readonly format?: string; readonly ref?: keyof typeof refs; - readonly enum?: ReadonlyArray<string>; + readonly enum?: ReadonlyArray<string | null>; readonly default?: (this['type'] extends TypeStringef ? StringDefToType<this['type']> : any) | null; readonly maxLength?: number; readonly minLength?: number; @@ -159,7 +161,7 @@ export type SchemaTypeDef<p extends Schema> = p['type'] extends 'integer' ? number : p['type'] extends 'number' ? number : p['type'] extends 'string' ? ( - p['enum'] extends readonly string[] ? + p['enum'] extends readonly (string | null)[] ? p['enum'][number] : p['format'] extends 'date-time' ? string : // Dateã«ã™ã‚‹ï¼Ÿï¼Ÿ string diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 311f875ba5..d00c8813c7 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js'; +import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -190,6 +190,12 @@ const $mutingsRepository: Provider = { inject: [DI.db], }; +const $renoteMutingsRepository: Provider = { + provide: DI.renoteMutingsRepository, + useFactory: (db: DataSource) => db.getRepository(RenoteMuting), + inject: [DI.db], +}; + const $blockingsRepository: Provider = { provide: DI.blockingsRepository, useFactory: (db: DataSource) => db.getRepository(Blocking), @@ -280,6 +286,12 @@ const $clipNotesRepository: Provider = { inject: [DI.db], }; +const $clipFavoritesRepository: Provider = { + provide: DI.clipFavoritesRepository, + useFactory: (db: DataSource) => db.getRepository(ClipFavorite), + inject: [DI.db], +}; + const $antennasRepository: Provider = { provide: DI.antennasRepository, useFactory: (db: DataSource) => db.getRepository(Antenna), @@ -423,6 +435,7 @@ const $roleAssignmentsRepository: Provider = { $notificationsRepository, $metasRepository, $mutingsRepository, + $renoteMutingsRepository, $blockingsRepository, $swSubscriptionsRepository, $hashtagsRepository, @@ -438,6 +451,7 @@ const $roleAssignmentsRepository: Provider = { $moderationLogsRepository, $clipsRepository, $clipNotesRepository, + $clipFavoritesRepository, $antennasRepository, $antennaNotesRepository, $promoNotesRepository, @@ -489,6 +503,7 @@ const $roleAssignmentsRepository: Provider = { $notificationsRepository, $metasRepository, $mutingsRepository, + $renoteMutingsRepository, $blockingsRepository, $swSubscriptionsRepository, $hashtagsRepository, @@ -504,6 +519,7 @@ const $roleAssignmentsRepository: Provider = { $moderationLogsRepository, $clipsRepository, $clipNotesRepository, + $clipFavoritesRepository, $antennasRepository, $antennaNotesRepository, $promoNotesRepository, diff --git a/packages/backend/src/models/entities/Antenna.ts b/packages/backend/src/models/entities/Antenna.ts index 5b2164ef17..e63e7f2c72 100644 --- a/packages/backend/src/models/entities/Antenna.ts +++ b/packages/backend/src/models/entities/Antenna.ts @@ -14,6 +14,10 @@ export class Antenna { public createdAt: Date; @Index() + @Column('timestamp with time zone') + public lastUsedAt: Date; + + @Index() @Column({ ...id(), comment: 'The owner ID.', @@ -83,4 +87,10 @@ export class Antenna { @Column('boolean') public notify: boolean; + + @Index() + @Column('boolean', { + default: true, + }) + public isActive: boolean; } diff --git a/packages/backend/src/models/entities/Clip.ts b/packages/backend/src/models/entities/Clip.ts index 57a310ac03..825a32c981 100644 --- a/packages/backend/src/models/entities/Clip.ts +++ b/packages/backend/src/models/entities/Clip.ts @@ -13,6 +13,12 @@ export class Clip { public createdAt: Date; @Index() + @Column('timestamp with time zone', { + nullable: true, + }) + public lastClippedAt: Date | null; + + @Index() @Column({ ...id(), comment: 'The owner ID.', diff --git a/packages/backend/src/models/entities/ClipFavorite.ts b/packages/backend/src/models/entities/ClipFavorite.ts new file mode 100644 index 0000000000..623471e671 --- /dev/null +++ b/packages/backend/src/models/entities/ClipFavorite.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { Clip } from './Clip.js'; + +@Entity() +@Index(['userId', 'clipId'], { unique: true }) +export class ClipFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public clipId: Clip['id']; + + @ManyToOne(type => Clip, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public clip: Clip | null; +} diff --git a/packages/backend/src/models/entities/Emoji.ts b/packages/backend/src/models/entities/Emoji.ts index 7332dd1857..dbb437d439 100644 --- a/packages/backend/src/models/entities/Emoji.ts +++ b/packages/backend/src/models/entities/Emoji.ts @@ -55,4 +55,9 @@ export class Emoji { array: true, length: 128, default: '{}', }) public aliases: string[]; + + @Column('varchar', { + length: 1024, nullable: true, + }) + public license: string | null; } diff --git a/packages/backend/src/models/entities/Flash.ts b/packages/backend/src/models/entities/Flash.ts index 192f7e7bc4..4ccc908a6a 100644 --- a/packages/backend/src/models/entities/Flash.ts +++ b/packages/backend/src/models/entities/Flash.ts @@ -43,7 +43,7 @@ export class Flash { public user: User | null; @Column('varchar', { - length: 32768, + length: 65536, }) public script: string; diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index 9d777c6236..57338ecbd2 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -12,7 +12,7 @@ export class Meta { public id: string; @Column('varchar', { - length: 128, nullable: true, + length: 1024, nullable: true, }) public name: string | null; @@ -25,7 +25,7 @@ export class Meta { * メンテナã®åå‰ */ @Column('varchar', { - length: 128, nullable: true, + length: 1024, nullable: true, }) public maintainerName: string | null; @@ -33,7 +33,7 @@ export class Meta { * メンテナã®é€£çµ¡å…ˆ */ @Column('varchar', { - length: 128, nullable: true, + length: 1024, nullable: true, }) public maintainerEmail: string | null; @@ -48,76 +48,68 @@ export class Meta { public useStarForReactionFallback: boolean; @Column('varchar', { - length: 64, array: true, default: '{}', + length: 1024, array: true, default: '{}', }) public langs: string[]; @Column('varchar', { - length: 256, array: true, default: '{}', + length: 1024, array: true, default: '{}', }) public pinnedUsers: string[]; @Column('varchar', { - length: 256, array: true, default: '{}', + length: 1024, array: true, default: '{}', }) public hiddenTags: string[]; @Column('varchar', { - length: 256, array: true, default: '{}', + length: 1024, array: true, default: '{}', }) public blockedHosts: string[]; @Column('varchar', { - length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}', - }) - public pinnedPages: string[]; - - @Column({ - ...id(), - nullable: true, + length: 1024, array: true, default: '{}', }) - public pinnedClipId: Clip['id'] | null; + public sensitiveWords: string[]; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public themeColor: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, - default: '/assets/ai.png', }) public mascotImageUrl: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public bannerUrl: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public backgroundImageUrl: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public logoImageUrl: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, - default: 'https://xn--931a.moe/aiart/yubitun.png', }) public errorImageUrl: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public iconUrl: string | null; @@ -150,13 +142,13 @@ export class Meta { public enableHcaptcha: boolean; @Column('varchar', { - length: 64, + length: 1024, nullable: true, }) public hcaptchaSiteKey: string | null; @Column('varchar', { - length: 64, + length: 1024, nullable: true, }) public hcaptchaSecretKey: string | null; @@ -167,13 +159,13 @@ export class Meta { public enableRecaptcha: boolean; @Column('varchar', { - length: 64, + length: 1024, nullable: true, }) public recaptchaSiteKey: string | null; @Column('varchar', { - length: 64, + length: 1024, nullable: true, }) public recaptchaSecretKey: string | null; @@ -184,13 +176,13 @@ export class Meta { public enableTurnstile: boolean; @Column('varchar', { - length: 64, + length: 1024, nullable: true, }) public turnstileSiteKey: string | null; @Column('varchar', { - length: 64, + length: 1024, nullable: true, }) public turnstileSecretKey: string | null; @@ -218,7 +210,7 @@ export class Meta { public enableSensitiveMediaDetectionForVideos: boolean; @Column('varchar', { - length: 128, + length: 1024, nullable: true, }) public summalyProxy: string | null; @@ -229,7 +221,7 @@ export class Meta { public enableEmail: boolean; @Column('varchar', { - length: 128, + length: 1024, nullable: true, }) public email: string | null; @@ -240,7 +232,7 @@ export class Meta { public smtpSecure: boolean; @Column('varchar', { - length: 128, + length: 1024, nullable: true, }) public smtpHost: string | null; @@ -251,13 +243,13 @@ export class Meta { public smtpPort: number | null; @Column('varchar', { - length: 128, + length: 1024, nullable: true, }) public smtpUser: string | null; @Column('varchar', { - length: 128, + length: 1024, nullable: true, }) public smtpPass: string | null; @@ -268,19 +260,19 @@ export class Meta { public enableServiceWorker: boolean; @Column('varchar', { - length: 128, + length: 1024, nullable: true, }) public swPublicKey: string | null; @Column('varchar', { - length: 128, + length: 1024, nullable: true, }) public swPrivateKey: string | null; @Column('varchar', { - length: 128, + length: 1024, nullable: true, }) public deeplAuthKey: string | null; @@ -291,20 +283,20 @@ export class Meta { public deeplIsPro: boolean; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) - public ToSUrl: string | null; + public termsOfServiceUrl: string | null; @Column('varchar', { - length: 512, + length: 1024, default: 'https://github.com/misskey-dev/misskey', nullable: false, }) public repositoryUrl: string; @Column('varchar', { - length: 512, + length: 1024, default: 'https://github.com/misskey-dev/misskey/issues/new', nullable: true, }) @@ -328,43 +320,43 @@ export class Meta { public useObjectStorage: boolean; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public objectStorageBucket: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public objectStoragePrefix: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public objectStorageBaseUrl: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public objectStorageEndpoint: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public objectStorageRegion: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public objectStorageAccessKey: string | null; @Column('varchar', { - length: 512, + length: 1024, nullable: true, }) public objectStorageSecretKey: string | null; diff --git a/packages/backend/src/models/entities/Note.ts b/packages/backend/src/models/entities/Note.ts index 82d042f0ce..df508b4dca 100644 --- a/packages/backend/src/models/entities/Note.ts +++ b/packages/backend/src/models/entities/Note.ts @@ -87,6 +87,11 @@ export class Note { }) public localOnly: boolean; + @Column('varchar', { + length: 64, nullable: true, + }) + public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | null; + @Column('smallint', { default: 0, }) diff --git a/packages/backend/src/models/entities/RenoteMuting.ts b/packages/backend/src/models/entities/RenoteMuting.ts new file mode 100644 index 0000000000..2f803a5fa8 --- /dev/null +++ b/packages/backend/src/models/entities/RenoteMuting.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; + +@Entity() +@Index(['muterId', 'muteeId'], { unique: true }) +export class RenoteMuting { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Muting.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The mutee user ID.', + }) + public muteeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public mutee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The muter user ID.', + }) + public muterId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public muter: User | null; +} diff --git a/packages/backend/src/models/entities/RetentionAggregation.ts b/packages/backend/src/models/entities/RetentionAggregation.ts index c79b762d71..c7bf38b3af 100644 --- a/packages/backend/src/models/entities/RetentionAggregation.ts +++ b/packages/backend/src/models/entities/RetentionAggregation.ts @@ -18,6 +18,12 @@ export class RetentionAggregation { }) public updatedAt: Date; + @Index({ unique: true }) + @Column('varchar', { + length: 512, nullable: false, + }) + public dateKey: string; + @Column({ ...id(), array: true, diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts index 399e9ead05..85ff266740 100644 --- a/packages/backend/src/models/entities/Role.ts +++ b/packages/backend/src/models/entities/Role.ts @@ -144,6 +144,12 @@ export class Role { }) public canEditMembersByModerator: boolean; + // UIã«è¡¨ç¤ºã™ã‚‹éš›ã®ä¸¦ã³é †ç”¨(大ãã„ã»ã©å…ˆé ) + @Column('integer', { + default: 0, + }) + public displayOrder: number; + @Column('jsonb', { default: { }, }) diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 25ed9b89d8..17083d7a01 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -13,6 +13,7 @@ import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js'; import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js'; import { Clip } from '@/models/entities/Clip.js'; import { ClipNote } from '@/models/entities/ClipNote.js'; +import { ClipFavorite } from '@/models/entities/ClipFavorite.js'; import { DriveFile } from '@/models/entities/DriveFile.js'; import { DriveFolder } from '@/models/entities/DriveFolder.js'; import { Emoji } from '@/models/entities/Emoji.js'; @@ -26,6 +27,7 @@ import { Meta } from '@/models/entities/Meta.js'; import { ModerationLog } from '@/models/entities/ModerationLog.js'; import { MutedNote } from '@/models/entities/MutedNote.js'; import { Muting } from '@/models/entities/Muting.js'; +import { RenoteMuting } from '@/models/entities/RenoteMuting.js'; import { Note } from '@/models/entities/Note.js'; import { NoteFavorite } from '@/models/entities/NoteFavorite.js'; import { NoteReaction } from '@/models/entities/NoteReaction.js'; @@ -80,6 +82,7 @@ export { ChannelNotePining, Clip, ClipNote, + ClipFavorite, DriveFile, DriveFolder, Emoji, @@ -93,6 +96,7 @@ export { ModerationLog, MutedNote, Muting, + RenoteMuting, Note, NoteFavorite, NoteReaction, @@ -146,6 +150,7 @@ export type ChannelFollowingsRepository = Repository<ChannelFollowing>; export type ChannelNotePiningsRepository = Repository<ChannelNotePining>; export type ClipsRepository = Repository<Clip>; export type ClipNotesRepository = Repository<ClipNote>; +export type ClipFavoritesRepository = Repository<ClipFavorite>; export type DriveFilesRepository = Repository<DriveFile>; export type DriveFoldersRepository = Repository<DriveFolder>; export type EmojisRepository = Repository<Emoji>; @@ -159,6 +164,7 @@ export type MetasRepository = Repository<Meta>; export type ModerationLogsRepository = Repository<ModerationLog>; export type MutedNotesRepository = Repository<MutedNote>; export type MutingsRepository = Repository<Muting>; +export type RenoteMutingsRepository = Repository<RenoteMuting>; export type NotesRepository = Repository<Note>; export type NoteFavoritesRepository = Repository<NoteFavorite>; export type NoteReactionsRepository = Repository<NoteReaction>; diff --git a/packages/backend/src/models/schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts index f0994e48f7..4483510610 100644 --- a/packages/backend/src/models/schema/antenna.ts +++ b/packages/backend/src/models/json-schema/antenna.ts @@ -75,6 +75,10 @@ export const packedAntennaSchema = { type: 'boolean', optional: false, nullable: false, }, + isActive: { + type: 'boolean', + optional: false, nullable: false, + }, hasUnreadNote: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/models/schema/app.ts b/packages/backend/src/models/json-schema/app.ts index c80dc81c33..c80dc81c33 100644 --- a/packages/backend/src/models/schema/app.ts +++ b/packages/backend/src/models/json-schema/app.ts diff --git a/packages/backend/src/models/schema/blocking.ts b/packages/backend/src/models/json-schema/blocking.ts index 5532322420..5532322420 100644 --- a/packages/backend/src/models/schema/blocking.ts +++ b/packages/backend/src/models/json-schema/blocking.ts diff --git a/packages/backend/src/models/schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts index 7f4f2a48b8..7f4f2a48b8 100644 --- a/packages/backend/src/models/schema/channel.ts +++ b/packages/backend/src/models/json-schema/channel.ts diff --git a/packages/backend/src/models/schema/clip.ts b/packages/backend/src/models/json-schema/clip.ts index f0ee2ce0c4..7310e59013 100644 --- a/packages/backend/src/models/schema/clip.ts +++ b/packages/backend/src/models/json-schema/clip.ts @@ -12,6 +12,11 @@ export const packedClipSchema = { optional: false, nullable: false, format: 'date-time', }, + lastClippedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, userId: { type: 'string', optional: false, nullable: false, @@ -34,5 +39,13 @@ export const packedClipSchema = { type: 'boolean', optional: false, nullable: false, }, + isFavorited: { + type: 'boolean', + optional: true, nullable: false, + }, + favoritedCount: { + type: 'number', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index 4359076612..4359076612 100644 --- a/packages/backend/src/models/schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts diff --git a/packages/backend/src/models/schema/drive-folder.ts b/packages/backend/src/models/json-schema/drive-folder.ts index 88cb8ab4a2..88cb8ab4a2 100644 --- a/packages/backend/src/models/schema/drive-folder.ts +++ b/packages/backend/src/models/json-schema/drive-folder.ts diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index c00c3dac1d..db4fd62cf6 100644 --- a/packages/backend/src/models/schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -59,5 +59,9 @@ export const packedEmojiDetailedSchema = { type: 'string', optional: false, nullable: false, }, + license: { + type: 'string', + optional: false, nullable: true, + }, }, } as const; diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 42d93dfac9..42d93dfac9 100644 --- a/packages/backend/src/models/schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts diff --git a/packages/backend/src/models/schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 8471a138ec..8471a138ec 100644 --- a/packages/backend/src/models/schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts diff --git a/packages/backend/src/models/schema/following.ts b/packages/backend/src/models/json-schema/following.ts index 2bcffbfc4d..2bcffbfc4d 100644 --- a/packages/backend/src/models/schema/following.ts +++ b/packages/backend/src/models/json-schema/following.ts diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/backend/src/models/json-schema/gallery-post.ts index fc503d4a64..fc503d4a64 100644 --- a/packages/backend/src/models/schema/gallery-post.ts +++ b/packages/backend/src/models/json-schema/gallery-post.ts diff --git a/packages/backend/src/models/schema/hashtag.ts b/packages/backend/src/models/json-schema/hashtag.ts index 98f8827640..98f8827640 100644 --- a/packages/backend/src/models/schema/hashtag.ts +++ b/packages/backend/src/models/json-schema/hashtag.ts diff --git a/packages/backend/src/models/schema/muting.ts b/packages/backend/src/models/json-schema/muting.ts index 3ab99e17e7..3ab99e17e7 100644 --- a/packages/backend/src/models/schema/muting.ts +++ b/packages/backend/src/models/json-schema/muting.ts diff --git a/packages/backend/src/models/schema/note-favorite.ts b/packages/backend/src/models/json-schema/note-favorite.ts index d133f7367d..d133f7367d 100644 --- a/packages/backend/src/models/schema/note-favorite.ts +++ b/packages/backend/src/models/json-schema/note-favorite.ts diff --git a/packages/backend/src/models/schema/note-reaction.ts b/packages/backend/src/models/json-schema/note-reaction.ts index 0d8fc5449b..0d8fc5449b 100644 --- a/packages/backend/src/models/schema/note-reaction.ts +++ b/packages/backend/src/models/json-schema/note-reaction.ts diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 72c0c62285..58ef425dcd 100644 --- a/packages/backend/src/models/schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -141,6 +141,10 @@ export const packedNoteSchema = { type: 'boolean', optional: true, nullable: false, }, + reactionAcceptance: { + type: 'string', + optional: false, nullable: true, + }, reactions: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/models/schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index d3f2405cdd..d3f2405cdd 100644 --- a/packages/backend/src/models/schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts diff --git a/packages/backend/src/models/schema/page.ts b/packages/backend/src/models/json-schema/page.ts index 55ba3ce7f7..55ba3ce7f7 100644 --- a/packages/backend/src/models/schema/page.ts +++ b/packages/backend/src/models/json-schema/page.ts diff --git a/packages/backend/src/models/schema/queue.ts b/packages/backend/src/models/json-schema/queue.ts index 7ceeda26af..7ceeda26af 100644 --- a/packages/backend/src/models/schema/queue.ts +++ b/packages/backend/src/models/json-schema/queue.ts diff --git a/packages/backend/src/models/json-schema/renote-muting.ts b/packages/backend/src/models/json-schema/renote-muting.ts new file mode 100644 index 0000000000..69ed8510da --- /dev/null +++ b/packages/backend/src/models/json-schema/renote-muting.ts @@ -0,0 +1,26 @@ +export const packedRenoteMutingSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + muteeId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + mutee: { + type: 'object', + optional: false, nullable: false, + ref: 'UserDetailed', + }, + }, +} as const; diff --git a/packages/backend/src/models/schema/user-list.ts b/packages/backend/src/models/json-schema/user-list.ts index 3ba5dc4a8a..3ba5dc4a8a 100644 --- a/packages/backend/src/models/schema/user-list.ts +++ b/packages/backend/src/models/json-schema/user-list.ts diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/json-schema/user.ts index c390018b46..e8a7212c52 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -29,7 +29,7 @@ export const packedUserLiteSchema = { nullable: true, optional: false, }, avatarBlurhash: { - type: 'any', + type: 'string', nullable: true, optional: false, }, isAdmin: { @@ -93,7 +93,7 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: true, optional: false, }, bannerBlurhash: { - type: 'any', + type: 'string', nullable: true, optional: false, }, isLocked: { @@ -234,6 +234,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: true, }, + isRenoteMuted: { + type: 'boolean', + nullable: false, optional: true, + }, //#endregion }, } as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index c2ee14b0f4..d5428805d1 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -21,6 +21,7 @@ import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js'; import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js'; import { Clip } from '@/models/entities/Clip.js'; import { ClipNote } from '@/models/entities/ClipNote.js'; +import { ClipFavorite } from '@/models/entities/ClipFavorite.js'; import { DriveFile } from '@/models/entities/DriveFile.js'; import { DriveFolder } from '@/models/entities/DriveFolder.js'; import { Emoji } from '@/models/entities/Emoji.js'; @@ -34,6 +35,7 @@ import { Meta } from '@/models/entities/Meta.js'; import { ModerationLog } from '@/models/entities/ModerationLog.js'; import { MutedNote } from '@/models/entities/MutedNote.js'; import { Muting } from '@/models/entities/Muting.js'; +import { RenoteMuting } from '@/models/entities/RenoteMuting.js'; import { Note } from '@/models/entities/Note.js'; import { NoteFavorite } from '@/models/entities/NoteFavorite.js'; import { NoteReaction } from '@/models/entities/NoteReaction.js'; @@ -139,6 +141,7 @@ export const entities = [ Following, FollowRequest, Muting, + RenoteMuting, Blocking, Note, NoteFavorite, @@ -163,6 +166,7 @@ export const entities = [ ModerationLog, Clip, ClipNote, + ClipFavorite, Antenna, AntennaNote, PromoNote, diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts index 02324c6cd4..e2720b4fe0 100644 --- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts +++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts @@ -7,6 +7,7 @@ import { bindThis } from '@/decorators.js'; import type { RetentionAggregationsRepository, UsersRepository } from '@/models/index.js'; import { deepClone } from '@/misc/clone.js'; import { IdService } from '@/core/IdService.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; @@ -49,13 +50,23 @@ export class AggregateRetentionProcessorService { }); const targetUserIds = targetUsers.map(u => u.id); - await this.retentionAggregationsRepository.insert({ - id: this.idService.genId(), - createdAt: now, - updatedAt: now, - userIds: targetUserIds, - usersCount: targetUserIds.length, - }); + try { + await this.retentionAggregationsRepository.insert({ + id: this.idService.genId(), + createdAt: now, + updatedAt: now, + dateKey, + userIds: targetUserIds, + usersCount: targetUserIds.length, + }); + } catch (err) { + if (isDuplicateKeyValueError(err)) { + this.logger.succ('Skip because it has already been processed by another worker.'); + done(); + return; + } + throw err; + } // 今日活動ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’å…¨ã¦å–å¾— const activeUsers = await this.usersRepository.findBy({ diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 7fd2cde9c0..9534454fd7 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; +import type { AntennaNotesRepository, AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; @@ -26,6 +26,9 @@ export class CleanProcessorService { @Inject(DI.mutedNotesRepository) private mutedNotesRepository: MutedNotesRepository, + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + @Inject(DI.antennaNotesRepository) private antennaNotesRepository: AntennaNotesRepository, @@ -55,8 +58,16 @@ export class CleanProcessorService { reason: 'word', }); - this.antennaNotesRepository.delete({ + this.mutedNotesRepository.delete({ id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), + reason: 'word', + }); + + // 7日以上使ã‚れã¦ãªã„ã‚¢ãƒ³ãƒ†ãƒŠã‚’åœæ¢ + this.antennasRepository.update({ + lastUsedAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 7))), + }, { + isActive: false, }); const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign') diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 2a053a12e0..43a92bb267 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -115,6 +115,18 @@ export class DeliverProcessorService { if (res instanceof StatusError) { // 4xx if (res.isClientError) { + // 相手ãŒé–‰éŽ–ã—ã¦ã„ã‚‹ã“ã¨ã‚’明示ã—ã¦ã„ã‚‹ãŸã‚ã€é…é€åœæ¢ã™ã‚‹ + if (job.data.isSharedInbox && res.statusCode === 410) { + this.federatedInstanceService.fetch(host).then(i => { + this.instancesRepository.update(i.id, { + isSuspended: true, + }); + this.federatedInstanceService.updateCachePartial(host, { + isSuspended: true, + }); + }); + return `${host} is gone`; + } // HTTPステータスコード4xxã¯ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã‚¨ãƒ©ãƒ¼ã§ã‚りã€ãれã¯ã¤ã¾ã‚Š // 何回å†é€ã—ã¦ã‚‚æˆåŠŸã™ã‚‹ã“ã¨ã¯ãªã„ã¨ã„ã†ã“ã¨ãªã®ã§ã‚¨ãƒ©ãƒ¼ã«ã¯ã—ãªã„ã§ãŠã return `${res.statusCode} ${res.statusMessage}`; diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 037dfa1a53..501ed4090a 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -3,11 +3,11 @@ import { DI } from '@/di-symbols.js'; import type { PollVotesRepository, NotesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { EndedPollNotificationJobData } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class EndedPollNotificationProcessorService { @@ -23,7 +23,7 @@ export class EndedPollNotificationProcessorService { @Inject(DI.pollVotesRepository) private pollVotesRepository: PollVotesRepository, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('ended-poll-notification'); @@ -47,7 +47,7 @@ export class EndedPollNotificationProcessorService { const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; for (const userId of userIds) { - this.createNotificationService.createNotification(userId, 'pollEnded', { + this.notificationService.createNotification(userId, 'pollEnded', { noteId: note.id, }); } diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index c65f0a97a0..e9330772b9 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -148,6 +148,7 @@ function serialize(favorite: NoteFavorite & { note: Note & { user: User } }, pol visibility: favorite.note.visibility, visibleUserIds: favorite.note.visibleUserIds, localOnly: favorite.note.localOnly, + reactionAcceptance: favorite.note.reactionAcceptance, uri: favorite.note.uri, url: favorite.note.url, user: { diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 3f4f16a2ec..2f74dd63cc 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -10,10 +10,10 @@ import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import type { Poll } from '@/models/entities/Poll.js'; import type { Note } from '@/models/entities/Note.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { DbUserJobData } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ExportNotesProcessorService { @@ -141,5 +141,6 @@ function serialize(note: Note, poll: Poll | null = null): Record<string, unknown visibility: note.visibility, visibleUserIds: note.visibleUserIds, localOnly: note.localOnly, + reactionAcceptance: note.reactionAcceptance, }; } diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 4ecf8daf74..ed96e9a525 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -102,6 +102,7 @@ export class ImportCustomEmojisProcessorService { host: null, aliases: emojiInfo.aliases, driveFile, + license: emojiInfo.license, }); } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 33d6f4eafa..41fe06b7c3 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -64,7 +64,7 @@ export class InboxProcessorService { const activity = job.data.activity; //#region Log - const info = Object.assign({}, activity) as any; + const info = Object.assign({}, activity); delete info['@context']; this.logger.debug(JSON.stringify(info, null, 2)); //#endregion diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 1214c9eb95..5d650c6864 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -12,6 +12,8 @@ export type DeliverJobData = { content: unknown; /** inbox URL to deliver */ to: string; + /** whether it is sharedInbox */ + isSharedInbox: boolean; }; export type InboxJobData = { diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 835657b625..794fa76d9e 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -2,7 +2,6 @@ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; -import fastifyStatic from '@fastify/static'; import rename from 'rename'; import type { Config } from '@/config.js'; import type { DriveFile, DriveFilesRepository } from '@/models/index.js'; @@ -60,11 +59,6 @@ export class FileServerService { done(); }); - fastify.register(fastifyStatic, { - root: _dirname, - serve: false, - }); - fastify.get('/files/app-default.jpg', (request, reply) => { const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); reply.header('Content-Type', 'image/jpeg'); @@ -297,7 +291,7 @@ export class FileServerService { }; } } else if ('static' in request.query) { - image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 280); + image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422); } else if ('preview' in request.query) { image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200); } else if ('badge' in request.query) { @@ -311,20 +305,20 @@ export class FileServerService { .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast .flatten({ background: '#000' }) .toColorspace('b-w'); - + const stats = await mask.clone().stats(); - + if (stats.entropy < 0.1) { // エントãƒãƒ”ーãŒã‚ã¾ã‚Šãªã„å ´åˆã¯404ã«ã™ã‚‹ throw new StatusError('Skip to provide badge', 404); } - + const data = sharp({ create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }, }) .pipelineColorspace('b-w') .boolean(await mask.png().toBuffer(), 'eor'); - + image = { data: await data.png().toBuffer(), ext: 'png', @@ -396,7 +390,7 @@ export class FileServerService { const { filename } = await this.downloadService.downloadUrl(url, path); const { mime, ext } = await this.fileInfoService.detectType(path); - + return { state: 'remote', mime, ext, diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 00a0d93093..364b46696d 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -100,7 +100,7 @@ export class NodeinfoServerService { email: meta.maintainerEmail, }, langs: meta.langs, - tosUrl: meta.ToSUrl, + tosUrl: meta.termsOfServiceUrl, repositoryUrl: meta.repositoryUrl, feedbackUrl: meta.feedbackUrl, disableRegistration: meta.disableRegistration, diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index a5a5f9e7f9..6bae0bafda 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -33,6 +33,7 @@ import { LocalTimelineChannelService } from './api/stream/channels/local-timelin import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { UserListChannelService } from './api/stream/channels/user-list.js'; +import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; @Module({ imports: [ @@ -72,6 +73,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js'; QueueStatsChannelService, ServerStatsChannelService, UserListChannelService, + OpenApiServerService, ], exports: [ ServerService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index e61383468c..3f116845cb 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -1,7 +1,9 @@ import cluster from 'node:cluster'; import * as fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import Fastify, { FastifyInstance } from 'fastify'; +import fastifyStatic from '@fastify/static'; import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Config } from '@/config.js'; @@ -21,6 +23,9 @@ import { StreamingApiServerService } from './api/StreamingApiServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js'; import { FileServerService } from './FileServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; +import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; + +const _dirname = fileURLToPath(new URL('.', import.meta.url)); @Injectable() export class ServerService implements OnApplicationShutdown { @@ -42,6 +47,7 @@ export class ServerService implements OnApplicationShutdown { private userEntityService: UserEntityService, private apiServerService: ApiServerService, + private openApiServerService: OpenApiServerService, private streamingApiServerService: StreamingApiServerService, private activityPubServerService: ActivityPubServerService, private wellKnownServerService: WellKnownServerService, @@ -71,7 +77,15 @@ export class ServerService implements OnApplicationShutdown { }); } + // Register non-serving static server so that the child services can use reply.sendFile. + // `root` here is just a placeholder and each call must use its own `rootPath`. + fastify.register(fastifyStatic, { + root: _dirname, + serve: false, + }); + fastify.register(this.apiServerService.createServer, { prefix: '/api' }); + fastify.register(this.openApiServerService.createServer); fastify.register(this.fileServerService.createServer); fastify.register(this.activityPubServerService.createServer); fastify.register(this.nodeinfoServerService.createServer); diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index f84a3aa59b..bf5cb20918 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -75,7 +75,7 @@ export class ApiCallService implements OnApplicationShutdown { } this.send(reply, res); }).catch((err: ApiError) => { - this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : 500, err); + this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err); }); if (user) { @@ -129,7 +129,7 @@ export class ApiCallService implements OnApplicationShutdown { }, request).then((res) => { this.send(reply, res); }).catch((err: ApiError) => { - this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : 500, err); + this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err); }); if (user) { @@ -321,7 +321,7 @@ export class ApiCallService implements OnApplicationShutdown { // API invoking return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => { - if (err instanceof ApiError) { + if (err instanceof ApiError || err instanceof AuthenticationError) { throw err; } else { const errId = uuid(); diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 115d60986c..b806ad5ca3 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -167,7 +167,7 @@ export class ApiServerService { // Make sure any unknown path under /api returns HTTP 404 Not Found, // because otherwise ClientServerService will return the base client HTML // page with HTTP 200. - fastify.get('*', (request, reply) => { + fastify.get('/*', (request, reply) => { reply.code(404); // Mock ApiCallService.send's error handling reply.send({ diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index d3e2219bd5..835e884193 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -42,6 +42,7 @@ import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js'; +import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js'; import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js'; import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; @@ -114,6 +115,9 @@ import * as ep___clips_list from './endpoints/clips/list.js'; import * as ep___clips_notes from './endpoints/clips/notes.js'; import * as ep___clips_show from './endpoints/clips/show.js'; import * as ep___clips_update from './endpoints/clips/update.js'; +import * as ep___clips_favorite from './endpoints/clips/favorite.js'; +import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js'; +import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js'; import * as ep___drive from './endpoints/drive.js'; import * as ep___drive_files from './endpoints/drive/files.js'; import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js'; @@ -220,10 +224,14 @@ import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; +import * as ep___emoji from './endpoints/emoji.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; import * as ep___mute_delete from './endpoints/mute/delete.js'; import * as ep___mute_list from './endpoints/mute/list.js'; +import * as ep___renoteMute_create from './endpoints/renote-mute/create.js'; +import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js'; +import * as ep___renoteMute_list from './endpoints/renote-mute/list.js'; import * as ep___my_apps from './endpoints/my/apps.js'; import * as ep___notes from './endpoints/notes.js'; import * as ep___notes_children from './endpoints/notes/children.js'; @@ -363,6 +371,7 @@ const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useCla const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default }; const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default }; const $admin_queue_inboxDelayed: Provider = { provide: 'ep:admin/queue/inbox-delayed', useClass: ep___admin_queue_inboxDelayed.default }; +const $admin_queue_promote: Provider = { provide: 'ep:admin/queue/promote', useClass: ep___admin_queue_promote.default }; const $admin_queue_stats: Provider = { provide: 'ep:admin/queue/stats', useClass: ep___admin_queue_stats.default }; const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass: ep___admin_relays_add.default }; const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default }; @@ -435,6 +444,9 @@ const $clips_list: Provider = { provide: 'ep:clips/list', useClass: ep___clips_l const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default }; const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default }; const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default }; +const $clips_favorite: Provider = { provide: 'ep:clips/favorite', useClass: ep___clips_favorite.default }; +const $clips_unfavorite: Provider = { provide: 'ep:clips/unfavorite', useClass: ep___clips_unfavorite.default }; +const $clips_myFavorites: Provider = { provide: 'ep:clips/my-favorites', useClass: ep___clips_myFavorites.default }; const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default }; const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default }; const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default }; @@ -541,10 +553,14 @@ const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default }; const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; +const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default }; const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default }; const $mute_list: Provider = { provide: 'ep:mute/list', useClass: ep___mute_list.default }; +const $renoteMute_create: Provider = { provide: 'ep:renote-mute/create', useClass: ep___renoteMute_create.default }; +const $renoteMute_delete: Provider = { provide: 'ep:renote-mute/delete', useClass: ep___renoteMute_delete.default }; +const $renoteMute_list: Provider = { provide: 'ep:renote-mute/list', useClass: ep___renoteMute_list.default }; const $my_apps: Provider = { provide: 'ep:my/apps', useClass: ep___my_apps.default }; const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default }; const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep___notes_children.default }; @@ -688,6 +704,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_queue_clear, $admin_queue_deliverDelayed, $admin_queue_inboxDelayed, + $admin_queue_promote, $admin_queue_stats, $admin_relays_add, $admin_relays_list, @@ -760,6 +777,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $clips_notes, $clips_show, $clips_update, + $clips_favorite, + $clips_unfavorite, + $clips_myFavorites, $drive, $drive_files, $drive_files_attachedNotes, @@ -866,10 +886,14 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_webhooks_delete, $meta, $emojis, + $emoji, $miauth_genToken, $mute_create, $mute_delete, $mute_list, + $renoteMute_create, + $renoteMute_delete, + $renoteMute_list, $my_apps, $notes, $notes_children, @@ -1007,6 +1031,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_queue_clear, $admin_queue_deliverDelayed, $admin_queue_inboxDelayed, + $admin_queue_promote, $admin_queue_stats, $admin_relays_add, $admin_relays_list, @@ -1079,6 +1104,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $clips_notes, $clips_show, $clips_update, + $clips_favorite, + $clips_unfavorite, + $clips_myFavorites, $drive, $drive_files, $drive_files_attachedNotes, @@ -1185,10 +1213,14 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_webhooks_delete, $meta, $emojis, + $emoji, $miauth_genToken, $mute_create, $mute_delete, $mute_list, + $renoteMute_create, + $renoteMute_delete, + $renoteMute_list, $my_apps, $notes, $notes_children, diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 41e8365d08..fbabf47aff 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import rndstr from 'rndstr'; import bcrypt from 'bcryptjs'; import { DI } from '@/di-symbols.js'; -import type { RegistrationTicketsRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; @@ -15,6 +15,7 @@ import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { bindThis } from '@/decorators.js'; import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; +import { IsNull } from 'typeorm'; @Injectable() export class SignupApiService { @@ -31,6 +32,9 @@ export class SignupApiService { @Inject(DI.userPendingsRepository) private userPendingsRepository: UserPendingsRepository, + @Inject(DI.usedUsernamesRepository) + private usedUsernamesRepository: UsedUsernamesRepository, + @Inject(DI.registrationTicketsRepository) private registrationTicketsRepository: RegistrationTicketsRepository, @@ -124,12 +128,21 @@ export class SignupApiService { } if (instance.emailRequiredForSignup) { + if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { + throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); + } + + // Check deleted username duplication + if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { + throw new FastifyReplyError(400, 'USED_USERNAME'); + } + const code = rndstr('a-z0-9', 16); - + // Generate hash of password const salt = await bcrypt.genSalt(8); const hash = await bcrypt.hash(password, salt); - + await this.userPendingsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), @@ -138,13 +151,13 @@ export class SignupApiService { username: username, password: hash, }); - + const link = `${this.config.url}/signup-complete/${code}`; - + this.emailService.sendEmail(emailAddress!, 'Signup', `To complete signup, please click this link:<br><a href="${link}">${link}</a>`, `To complete signup, please click this link: ${link}`); - + reply.code(204); return; } else { diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 487eef2d50..13526f277d 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -3,17 +3,17 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import * as websocket from 'websocket'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, RenoteMutingsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; +import { bindThis } from '@/decorators.js'; import { AuthenticateService } from './AuthenticateService.js'; import MainStreamConnection from './stream/index.js'; import { ChannelsService } from './stream/ChannelsService.js'; import type { ParsedUrlQuery } from 'querystring'; import type * as http from 'node:http'; -import { bindThis } from '@/decorators.js'; @Injectable() export class StreamingApiServerService { @@ -33,6 +33,9 @@ export class StreamingApiServerService { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, + @Inject(DI.renoteMutingsRepository) + private renoteMutingsRepository: RenoteMutingsRepository, + @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -84,6 +87,7 @@ export class StreamingApiServerService { const main = new MainStreamConnection( this.followingsRepository, this.mutingsRepository, + this.renoteMutingsRepository, this.blockingsRepository, this.channelFollowingsRepository, this.userProfilesRepository, diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index ed283eb834..1555a3ca46 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import Ajv from 'ajv'; -import type { Schema, SchemaType } from '@/misc/schema.js'; +import type { Schema, SchemaType } from '@/misc/json-schema.js'; import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import { ApiError } from './error.js'; diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 4f521148e0..f6fc79fc70 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -1,4 +1,4 @@ -import type { Schema } from '@/misc/schema.js'; +import type { Schema } from '@/misc/json-schema.js'; import { RolePolicies } from '@/core/RoleService.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; @@ -42,6 +42,7 @@ import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js'; +import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js'; import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js'; import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; @@ -114,6 +115,9 @@ import * as ep___clips_list from './endpoints/clips/list.js'; import * as ep___clips_notes from './endpoints/clips/notes.js'; import * as ep___clips_show from './endpoints/clips/show.js'; import * as ep___clips_update from './endpoints/clips/update.js'; +import * as ep___clips_favorite from './endpoints/clips/favorite.js'; +import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js'; +import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js'; import * as ep___drive from './endpoints/drive.js'; import * as ep___drive_files from './endpoints/drive/files.js'; import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js'; @@ -220,10 +224,14 @@ import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; +import * as ep___emoji from './endpoints/emoji.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; import * as ep___mute_delete from './endpoints/mute/delete.js'; import * as ep___mute_list from './endpoints/mute/list.js'; +import * as ep___renoteMute_create from './endpoints/renote-mute/create.js'; +import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js'; +import * as ep___renoteMute_list from './endpoints/renote-mute/list.js'; import * as ep___my_apps from './endpoints/my/apps.js'; import * as ep___notes from './endpoints/notes.js'; import * as ep___notes_children from './endpoints/notes/children.js'; @@ -361,6 +369,7 @@ const eps = [ ['admin/queue/clear', ep___admin_queue_clear], ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed], ['admin/queue/inbox-delayed', ep___admin_queue_inboxDelayed], + ['admin/queue/promote', ep___admin_queue_promote], ['admin/queue/stats', ep___admin_queue_stats], ['admin/relays/add', ep___admin_relays_add], ['admin/relays/list', ep___admin_relays_list], @@ -433,6 +442,9 @@ const eps = [ ['clips/notes', ep___clips_notes], ['clips/show', ep___clips_show], ['clips/update', ep___clips_update], + ['clips/favorite', ep___clips_favorite], + ['clips/unfavorite', ep___clips_unfavorite], + ['clips/my-favorites', ep___clips_myFavorites], ['drive', ep___drive], ['drive/files', ep___drive_files], ['drive/files/attached-notes', ep___drive_files_attachedNotes], @@ -539,10 +551,14 @@ const eps = [ ['i/webhooks/delete', ep___i_webhooks_delete], ['meta', ep___meta], ['emojis', ep___emojis], + ['emoji', ep___emoji], ['miauth/gen-token', ep___miauth_genToken], ['mute/create', ep___mute_create], ['mute/delete', ep___mute_delete], ['mute/list', ep___mute_list], + ['renote-mute/create', ep___renoteMute_create], + ['renote-mute/delete', ep___renoteMute_delete], + ['renote-mute/list', ep___renoteMute_list], ['my/apps', ep___my_apps], ['notes', ep___notes], ['notes/children', ep___notes_children], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 0cc60e9191..4e4f845b0b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -53,7 +53,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { }); } - await this.db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache?.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiUpdated', { emojis: await this.emojiEntityService.packDetailedMany(ps.ids), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 04c58050ff..2fb3e489e7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -56,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { category: null, aliases: [], host: null, + license: null, }); this.moderationLogService.insertModerationLog(me, 'addEmoji', { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 8885a40fd9..fea11a67d6 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -87,9 +87,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { originalUrl: driveFile.url, publicUrl: driveFile.webpublicUrl ?? driveFile.url, type: driveFile.webpublicType ?? driveFile.type, + license: emoji.license, }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); - await this.db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache?.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiAdded', { emoji: await this.emojiEntityService.packDetailed(copied.id), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index f298baaedf..84aad020af 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { for (const emoji of emojis) { await this.emojisRepository.delete(emoji.id); - await this.db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache?.remove(['meta_emojis']); this.moderationLogService.insertModerationLog(me, 'deleteEmoji', { emoji: emoji, }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index a5fbe3f4ea..90a5856a1b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { await this.emojisRepository.delete(emoji.id); - await this.db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache?.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiDeleted', { emojis: [await this.emojiEntityService.packDetailed(emoji)], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index 66547024f7..3935183502 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -53,7 +53,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { }); } - await this.db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache?.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiUpdated', { emojis: await this.emojiEntityService.packDetailedMany(ps.ids), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index c8992eeb04..6a875f9c83 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { aliases: ps.aliases, }); - await this.db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache?.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiUpdated', { emojis: await this.emojiEntityService.packDetailedMany(ps.ids), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index 8a538c1003..d3b999c0ed 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { category: ps.category, }); - await this.db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache?.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiUpdated', { emojis: await this.emojiEntityService.packDetailedMany(ps.ids), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 809bf77d6b..1c649db93e 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -19,6 +19,11 @@ export const meta = { code: 'NO_SUCH_EMOJI', id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8', }, + alreadyexistsemoji: { + message: 'Emoji already exists', + code: 'EMOJI_ALREADY_EXISTS', + id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8', + }, }, } as const; @@ -26,7 +31,7 @@ export const paramDef = { type: 'object', properties: { id: { type: 'string', format: 'misskey:id' }, - name: { type: 'string' }, + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, category: { type: 'string', nullable: true, @@ -35,6 +40,7 @@ export const paramDef = { aliases: { type: 'array', items: { type: 'string', } }, + license: { type: 'string', nullable: true }, }, required: ['id', 'name', 'aliases'], } as const; @@ -56,17 +62,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { ) { super(meta, paramDef, async (ps, me) => { const emoji = await this.emojisRepository.findOneBy({ id: ps.id }); - + const emojiname = await this.emojisRepository.findOneBy({ name: ps.name }); if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - + if (emojiname != null && emojiname.id !== ps.id) throw new ApiError(meta.errors.alreadyexistsemoji); await this.emojisRepository.update(emoji.id, { updatedAt: new Date(), name: ps.name, category: ps.category, aliases: ps.aliases, + license: ps.license, }); - await this.db.queryResultCache!.remove(['meta_emojis']); + await this.db.queryResultCache?.remove(['meta_emojis']); const updated = await this.emojiEntityService.packDetailed(emoji.id); diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 9eef1b29c5..ce7e0d569d 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -110,6 +110,14 @@ export const meta = { optional: false, nullable: false, }, }, + sensitiveWords: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, hcaptchaSecretKey: { type: 'string', optional: true, nullable: true, @@ -266,7 +274,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { uri: this.config.url, description: instance.description, langs: instance.langs, - tosUrl: instance.ToSUrl, + tosUrl: instance.termsOfServiceUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, disableRegistration: instance.disableRegistration, @@ -290,13 +298,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, - pinnedPages: instance.pinnedPages, - pinnedClipId: instance.pinnedClipId, cacheRemoteFiles: instance.cacheRemoteFiles, useStarForReactionFallback: instance.useStarForReactionFallback, pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, + sensitiveWords: instance.sensitiveWords, hcaptchaSecretKey: instance.hcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts new file mode 100644 index 0000000000..4e57e6613e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + type: { type: 'string', enum: ['deliver', 'inbox'] }, + }, + required: ['type'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private moderationLogService: ModerationLogService, + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + let delayedQueues; + + switch (ps.type) { + case 'deliver': + delayedQueues = await this.queueService.deliverQueue.getDelayed(); + for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { + const queue = delayedQueues[queueIndex]; + await queue.promote(); + } + break; + + case 'inbox': + delayedQueues = await this.queueService.inboxQueue.getDelayed(); + for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { + const queue = delayedQueues[queueIndex]; + await queue.promote(); + } + break; + } + + this.moderationLogService.insertModerationLog(me, 'promoteQueue'); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index d0d52089e6..aead894611 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { const actor = await this.instanceActorService.getInstanceActor(); const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); - this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox); + this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false); } await this.abuseUserReportsRepository.update(report.id, { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index df60c6be94..1359894634 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -27,6 +27,7 @@ export const paramDef = { isAdministrator: { type: 'boolean' }, asBadge: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' }, + displayOrder: { type: 'number' }, policies: { type: 'object', }, @@ -43,6 +44,7 @@ export const paramDef = { 'isAdministrator', 'asBadge', 'canEditMembersByModerator', + 'displayOrder', 'policies', ], } as const; @@ -76,6 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { isModerator: ps.isModerator, asBadge: ps.asBadge, canEditMembersByModerator: ps.canEditMembersByModerator, + displayOrder: ps.displayOrder, policies: ps.policies, }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index b939ccdbf9..37b68c4c41 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -35,6 +35,7 @@ export const paramDef = { isAdministrator: { type: 'boolean' }, asBadge: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' }, + displayOrder: { type: 'number' }, policies: { type: 'object', }, @@ -52,6 +53,7 @@ export const paramDef = { 'isAdministrator', 'asBadge', 'canEditMembersByModerator', + 'displayOrder', 'policies', ], } as const; @@ -85,6 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { isAdministrator: ps.isAdministrator, asBadge: ps.asBadge, canEditMembersByModerator: ps.canEditMembersByModerator, + displayOrder: ps.displayOrder, policies: ps.policies, }); const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId }); diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index a7531aae89..2f23aca243 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -27,6 +27,9 @@ export const paramDef = { blockedHosts: { type: 'array', nullable: true, items: { type: 'string', } }, + sensitiveWords: { type: 'array', nullable: true, items: { + type: 'string', + } }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, @@ -56,10 +59,6 @@ export const paramDef = { proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true }, maintainerName: { type: 'string', nullable: true }, maintainerEmail: { type: 'string', nullable: true }, - pinnedPages: { type: 'array', items: { - type: 'string', - } }, - pinnedClipId: { type: 'string', format: 'misskey:id', nullable: true }, langs: { type: 'array', items: { type: 'string', } }, @@ -131,6 +130,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase()); } + if (Array.isArray(ps.sensitiveWords)) { + set.sensitiveWords = ps.sensitiveWords.filter(Boolean); + } + if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } @@ -247,14 +250,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { set.langs = ps.langs.filter(Boolean); } - if (Array.isArray(ps.pinnedPages)) { - set.pinnedPages = ps.pinnedPages.filter(Boolean); - } - - if (ps.pinnedClipId !== undefined) { - set.pinnedClipId = ps.pinnedClipId; - } - if (ps.summalyProxy !== undefined) { set.summalyProxy = ps.summalyProxy; } @@ -304,7 +299,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } if (ps.tosUrl !== undefined) { - set.ToSUrl = ps.tosUrl; + set.termsOfServiceUrl = ps.tosUrl; } if (ps.repositoryUrl !== undefined) { diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index bc5d249ae5..d147ddb7f1 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -79,6 +79,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { + if (ps.keywords.length === 0) { + throw new Error('invalid param'); + } + const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id, }); @@ -99,9 +103,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } } + const now = new Date(); + const antenna = await this.antennasRepository.insert({ id: this.idService.genId(), - createdAt: new Date(), + createdAt: now, + lastUsedAt: now, userId: me.id, name: ps.name, src: ps.src, diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index fbb5acf617..039ba1115a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -101,6 +101,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { this.noteReadService.read(me.id, notes); } + this.antennasRepository.update(antenna.id, { + lastUsedAt: new Date(), + }); + return await this.noteEntityService.packMany(notes, me); }); } diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 61e05531e6..a103d4196a 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -5,7 +5,7 @@ import type { UsersRepository, NotesRepository } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; import type { LocalUser, User } from '@/models/entities/User.js'; import { isActor, isPost, getApId } from '@/core/activitypub/type.js'; -import type { SchemaType } from '@/misc/schema.js'; +import type { SchemaType } from '@/misc/json-schema.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { MetaService } from '@/core/MetaService.js'; diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index d006e89bd2..a86cc2565a 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -4,6 +4,7 @@ import type { DriveFilesRepository, ChannelsRepository } from '@/models/index.js import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['channels'], @@ -61,7 +62,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private driveFilesRepository: DriveFilesRepository, private channelEntityService: ChannelEntityService, - ) { + + private roleService: RoleService, + ) { super(meta, paramDef, async (ps, me) => { const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, @@ -71,7 +74,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { throw new ApiError(meta.errors.noSuchChannel); } - if (channel.userId !== me.id) { + const iAmModerator = await this.roleService.isModerator(me); + if (channel.userId !== me.id && !iAmModerator) { throw new ApiError(meta.errors.accessDenied); } diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index f3f9c3477f..b9d8dce47a 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -106,6 +106,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { noteId: note.id, clipId: clip.id, }); + + await this.clipsRepository.update(clip.id, { + lastClippedAt: new Date(), + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index c095de702c..a770dc986d 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { description: ps.description, }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0])); - return await this.clipEntityService.pack(clip); + return await this.clipEntityService.pack(clip, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts new file mode 100644 index 0000000000..6addf743a2 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts @@ -0,0 +1,76 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['clip'], + + requireCredential: true, + + kind: 'write:clip-favorite', + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', + }, + + alreadyFavorited: { + message: 'The clip has already been favorited.', + code: 'ALREADY_FAVORITED', + id: '92658936-c625-4273-8326-2d790129256e', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + clipId: { type: 'string', format: 'misskey:id' }, + }, + required: ['clipId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipFavoritesRepository) + private clipFavoritesRepository: ClipFavoritesRepository, + + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const clip = await this.clipsRepository.findOneBy({ id: ps.clipId }); + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + if ((clip.userId !== me.id) && !clip.isPublic) { + throw new ApiError(meta.errors.noSuchClip); + } + + const exist = await this.clipFavoritesRepository.findOneBy({ + clipId: clip.id, + userId: me.id, + }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyFavorited); + } + + await this.clipFavoritesRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + clipId: clip.id, + userId: me.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts index 63ca069364..3b8deab709 100644 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ b/packages/backend/src/server/api/endpoints/clips/list.ts @@ -42,7 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { userId: me.id, }); - return await Promise.all(clips.map(x => this.clipEntityService.pack(x))); + return await this.clipEntityService.packMany(clips, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts new file mode 100644 index 0000000000..fc727e93bd --- /dev/null +++ b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts @@ -0,0 +1,52 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { ClipFavoritesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; + +export const meta = { + tags: ['account', 'clip'], + + requireCredential: true, + + kind: 'read:clip-favorite', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Clip', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.clipFavoritesRepository) + private clipFavoritesRepository: ClipFavoritesRepository, + + private clipEntityService: ClipEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.clipFavoritesRepository.createQueryBuilder('favorite') + .andWhere('favorite.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('favorite.clip', 'clip'); + + const favorites = await query + .getMany(); + + return this.clipEntityService.packMany(favorites.map(x => x.clip!), me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts index e6d3f4f1f8..99d630a9b5 100644 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ b/packages/backend/src/server/api/endpoints/clips/show.ts @@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { throw new ApiError(meta.errors.noSuchClip); } - return await this.clipEntityService.pack(clip); + return await this.clipEntityService.pack(clip, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts new file mode 100644 index 0000000000..244843d50f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts @@ -0,0 +1,65 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['clip'], + + requireCredential: true, + + kind: 'write:clip-favorite', + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: '2603966e-b865-426c-94a7-af4a01241dc1', + }, + + notFavorited: { + message: 'You have not favorited the clip.', + code: 'NOT_FAVORITED', + id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + clipId: { type: 'string', format: 'misskey:id' }, + }, + required: ['clipId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipFavoritesRepository) + private clipFavoritesRepository: ClipFavoritesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const clip = await this.clipsRepository.findOneBy({ id: ps.clipId }); + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + const exist = await this.clipFavoritesRepository.findOneBy({ + clipId: clip.id, + userId: me.id, + }); + + if (exist == null) { + throw new ApiError(meta.errors.notFavorited); + } + + await this.clipFavoritesRepository.delete(exist.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 597b67c442..a103c3f7d3 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { isPublic: ps.isPublic, }); - return await this.clipEntityService.pack(clip.id); + return await this.clipEntityService.pack(clip.id, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index f6fad50fd9..4609307774 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -31,6 +31,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, + sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] }, }, required: [], } as const; @@ -63,6 +64,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } } + switch (ps.sort) { + case '+createdAt': query.orderBy('file.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('file.createdAt', 'ASC'); break; + case '+name': query.orderBy('file.name', 'DESC'); break; + case '-name': query.orderBy('file.name', 'ASC'); break; + case '+size': query.orderBy('file.size', 'DESC'); break; + case '-size': query.orderBy('file.size', 'ASC'); break; + } + const files = await query.take(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); diff --git a/packages/backend/src/server/api/endpoints/emoji.ts b/packages/backend/src/server/api/endpoints/emoji.ts new file mode 100644 index 0000000000..681d3e649e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/emoji.ts @@ -0,0 +1,56 @@ +import { IsNull } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import type { EmojisRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: false, + allowGet: true, + cacheSec: 3600, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'EmojiDetailed', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + required: ['name'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private emojiEntityService: EmojiEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const emoji = await this.emojisRepository.findOneOrFail({ + where: { + name: ps.name, + host: IsNull(), + }, + }); + + return this.emojiEntityService.packDetailed(emoji); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 325b758358..0711fe4a57 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -23,24 +23,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, - properties: { - name: { - type: 'string', - optional: false, nullable: false, - }, - aliases: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - category: { - type: 'string', - optional: false, nullable: true, - }, - }, + ref: 'EmojiSimple', }, }, }, diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 60b24e9585..061c6eb5be 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -76,9 +76,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { if (typeof ps.blocked === 'boolean') { const meta = await this.metaService.fetch(true); if (ps.blocked) { - query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts }); + query.andWhere(meta.blockedHosts.length === 0 ? '1=0' : 'instance.host IN (:...blocks)', { blocks: meta.blockedHosts }); } else { - query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts }); + query.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts }); } } diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index 6beef5ab85..a3e3e02a12 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -3,6 +3,7 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/index.js' import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '../error.js'; export const meta = { tags: ['account'], @@ -14,6 +15,15 @@ export const meta = { optional: false, nullable: false, ref: 'MeDetailed', }, + + errors: { + userIsDeleted: { + message: 'User is deleted.', + code: 'USER_IS_DELETED', + id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a', + kind: 'permission', + }, + } } as const; export const paramDef = { @@ -41,13 +51,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`; // 渡ã£ã¦ãã¦ã„ã‚‹ user ã¯ã‚ャッシュã•れã¦ã„ã¦å¤ã„å¯èƒ½æ€§ãŒã‚ã‚‹ã®ã§æ”¹ã‚ã¦å–å¾— - const userProfile = await this.userProfilesRepository.findOneOrFail({ + const userProfile = await this.userProfilesRepository.findOne({ where: { userId: user.id, }, relations: ['user'], }); + if (userProfile == null) { + throw new ApiError(meta.errors.userIsDeleted); + } + if (!userProfile.loggedInDates.includes(today)) { this.userProfilesRepository.update({ userId: user.id }, { loggedInDates: [...userProfile.loggedInDates, today], diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index cdb314a873..37974ce2a3 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -276,7 +276,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { uri: this.config.url, description: instance.description, langs: instance.langs, - tosUrl: instance.ToSUrl, + tosUrl: instance.termsOfServiceUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, disableRegistration: instance.disableRegistration, @@ -315,8 +315,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { mediaProxy: this.config.mediaProxy, ...(ps.detail ? { - pinnedPages: instance.pinnedPages, - pinnedClipId: instance.pinnedClipId, cacheRemoteFiles: instance.cacheRemoteFiles, requireSetup: (await this.usersRepository.countBy({ host: IsNull(), diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index d5caec6e1d..0a5542f497 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -4,8 +4,8 @@ import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['clips', 'notes'], @@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { isPublic: true, }); - return await Promise.all(clips.map(x => this.clipEntityService.pack(x))); + return await this.clipEntityService.packMany(clips, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 786ad103b0..69fafcb9c7 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -97,6 +97,7 @@ export const paramDef = { } }, cw: { type: 'string', nullable: true, maxLength: 100 }, localOnly: { type: 'boolean', default: false }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, noExtractHashtags: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false }, @@ -110,7 +111,7 @@ export const paramDef = { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, - nullable: false + nullable: false, }, fileIds: { type: 'array', @@ -280,6 +281,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { renote, cw: ps.cw, localOnly: ps.localOnly, + reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, visibleUsers, channel, diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index cf939f6631..6bf17b222a 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { let notes = await query .orderBy('note.score', 'DESC') - .take(50) + .take(100) .getMany(); notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 5d0cdc3fca..9118d33936 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -89,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); } if (ps.withFiles) { diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 2819abb125..8a7ec65ab4 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -8,6 +8,7 @@ import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; +import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -69,6 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private metaService: MetaService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const policies = await this.roleService.getUserPolicies(me.id); @@ -83,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30æ—¥å‰ã¾ã§ + .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10æ—¥å‰ã¾ã§ .andWhere(new Brackets(qb => { qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id }) .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); @@ -107,6 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.includeMyRenotes === false) { query.andWhere(new Brackets(qb => { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 18ed6d4e21..8c1c07a9f4 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -8,6 +8,7 @@ import { MetaService } from '@/core/MetaService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; +import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -65,6 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private metaService: MetaService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const policies = await this.roleService.getUserPolicies(me ? me.id : null); @@ -75,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { //#region Construct query const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30æ—¥å‰ã¾ã§ + .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10æ—¥å‰ã¾ã§ .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') @@ -95,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateMutedNoteQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me); + if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index b9e06a7834..2a44dc537e 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -8,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js'; import { PollService } from '@/core/PollService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { DI } from '@/di-symbols.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { ApiError } from '../../../error.js'; @@ -89,7 +88,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private pollService: PollService, private apRendererService: ApRendererService, private globalEventService: GlobalEventService, - private createNotificationService: CreateNotificationService, private userBlockingService: UserBlockingService, ) { super(meta, paramDef, async (ps, me) => { @@ -161,7 +159,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { if (note.userHost != null) { const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as RemoteUser; - this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox); + this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox, false); } // リモートフォãƒãƒ¯ãƒ¼ã«Updateé…ä¿¡ diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index ef47a3004d..5db5b6267f 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -6,6 +6,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -23,6 +25,11 @@ export const meta = { }, errors: { + unavailable: { + message: 'Search of notes unavailable.', + code: 'UNAVAILABLE', + id: '0b44998d-77aa-4427-80d0-d2c9b8523011', + }, }, } as const; @@ -59,8 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private noteEntityService: NoteEntityService, private queryService: QueryService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { + const policies = await this.roleService.getUserPolicies(me ? me.id : null); + if (!policies.canSearchNotes) { + throw new ApiError(meta.errors.unavailable); + } + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); if (ps.userId) { diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index e6de087c4a..d9e72d2603 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -6,6 +6,7 @@ import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['notes'], @@ -56,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private noteEntityService: NoteEntityService, private queryService: QueryService, private activeUsersChart: ActiveUsersChart, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const followees = await this.followingsRepository.createQueryBuilder('following') @@ -66,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { //#region Construct query const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30æ—¥å‰ã¾ã§ + .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10æ—¥å‰ã¾ã§ .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -93,6 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.includeMyRenotes === false) { query.andWhere(new Brackets(qb => { diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 2e63eee263..4102a924ad 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; export const meta = { tags: ['notifications'], @@ -27,10 +27,10 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { constructor( - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, ) { super(meta, paramDef, async (ps, user, token) => { - this.createNotificationService.createNotification(user.id, 'app', { + this.notificationService.createNotification(user.id, 'app', { appAccessTokenId: token ? token.id : null, customBody: ps.body, customHeader: ps.header, diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts new file mode 100644 index 0000000000..051a005b67 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -0,0 +1,99 @@ +import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import type { RenoteMutingsRepository } from '@/models/index.js'; +import type { RenoteMuting } from '@/models/entities/RenoteMuting.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + kind: 'write:mutes', + + limit: { + duration: ms('1hour'), + max: 20, + }, + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '5e0a5dff-1e94-4202-87ae-4d9c89eb2271', + }, + + muteeIsYourself: { + message: 'Mutee is yourself.', + code: 'MUTEE_IS_YOURSELF', + id: '37285718-52f7-4aef-b7de-c38b8e8a8420', + }, + + alreadyMuting: { + message: 'You are already muting that user.', + code: 'ALREADY_MUTING', + id: 'ccfecbe4-1f1c-4fc2-8a3d-c3ffee61cb7b', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.renoteMutingsRepository) + private renoteMutingsRepository: RenoteMutingsRepository, + + private globalEventService: GlobalEventService, + private getterService: GetterService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const muter = me; + + // 自分自身 + if (me.id === ps.userId) { + throw new ApiError(meta.errors.muteeIsYourself); + } + + // Get mutee + const mutee = await getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check if already muting + const exist = await this.renoteMutingsRepository.findOneBy({ + muterId: muter.id, + muteeId: mutee.id, + }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyMuting); + } + + // Create mute + await this.renoteMutingsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + muterId: muter.id, + muteeId: mutee.id, + } as RenoteMuting); + + // publishUserEvent(user.id, 'mute', mutee); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts new file mode 100644 index 0000000000..51a895fb7e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -0,0 +1,87 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RenoteMutingsRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + kind: 'write:mutes', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '9b6728cf-638c-4aa1-bedb-e07d8101474d', + }, + + muteeIsYourself: { + message: 'Mutee is yourself.', + code: 'MUTEE_IS_YOURSELF', + id: '619b1314-0850-4597-a242-e245f3da42af', + }, + + notMuting: { + message: 'You are not muting that user.', + code: 'NOT_MUTING', + id: '2e4ef874-8bf0-4b4b-b069-4598f6d05817', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.renoteMutingsRepository) + private renoteMutingsRepository: RenoteMutingsRepository, + + private globalEventService: GlobalEventService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const muter = me; + + // Check if the mutee is yourself + if (me.id === ps.userId) { + throw new ApiError(meta.errors.muteeIsYourself); + } + + // Get mutee + const mutee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check not muting + const exist = await this.renoteMutingsRepository.findOneBy({ + muterId: muter.id, + muteeId: mutee.id, + }); + + if (exist == null) { + throw new ApiError(meta.errors.notMuting); + } + + // Delete mute + await this.renoteMutingsRepository.delete({ + id: exist.id, + }); + + // publishUserEvent(user.id, 'unmute', mutee); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/renote-mute/list.ts b/packages/backend/src/server/api/endpoints/renote-mute/list.ts new file mode 100644 index 0000000000..b2d7addb64 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/renote-mute/list.ts @@ -0,0 +1,57 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RenoteMutingsRepository } from '@/models/index.js'; +import { QueryService } from '@/core/QueryService.js'; +import { RenoteMutingEntityService } from '@/core/entities/RenoteMutingEntityService.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + kind: 'read:mutes', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'RenoteMuting', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.renoteMutingsRepository) + private renoteMutingsRepository: RenoteMutingsRepository, + + private renoteMutingEntityService: RenoteMutingEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.renoteMutingsRepository.createQueryBuilder('muting'), ps.sinceId, ps.untilId) + .andWhere('muting.muterId = :meId', { meId: me.id }); + + const mutings = await query + .take(ps.limit) + .getMany(); + + return await this.renoteMutingEntityService.packMany(mutings, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index e3fd0920c9..c5aa93baaf 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .take(ps.limit) .getMany(); - return await this.clipEntityService.packMany(clips); + return await this.clipEntityService.packMany(clips, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index ac9104bf92..3267c18846 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -50,6 +50,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + isRenoteMuted: { + type: 'boolean', + optional: false, nullable: false, + }, }, }, { @@ -91,6 +95,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + isRenoteMuted: { + type: 'boolean', + optional: false, nullable: false, + }, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 29f24b045a..ba432c273b 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -48,6 +48,7 @@ export const meta = { message: 'No such user.', code: 'NO_SUCH_USER', id: '4362f8dc-731f-4ad8-a694-be5a88922a24', + httpStatusCode: 404, }, }, } as const; diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index 347d5650ad..34f4521606 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -1,4 +1,4 @@ -type E = { message: string, code: string, id: string, kind?: 'client' | 'server', httpStatusCode?: number }; +type E = { message: string, code: string, id: string, kind?: 'client' | 'server' | 'permission', httpStatusCode?: number }; export class ApiError extends Error { public message: string; diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts new file mode 100644 index 0000000000..e804ba276c --- /dev/null +++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts @@ -0,0 +1,31 @@ +import { fileURLToPath } from 'node:url'; +import { Inject, Injectable } from '@nestjs/common'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { genOpenapiSpec } from './gen-spec.js'; +import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; + +const staticAssets = fileURLToPath(new URL('../../../../assets/', import.meta.url)); + +@Injectable() +export class OpenApiServerService { + constructor( + @Inject(DI.config) + private config: Config, + ) { + } + + @bindThis + public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) { + fastify.get('/api-doc', async (_request, reply) => { + reply.header('Cache-Control', 'public, max-age=86400'); + return await reply.sendFile('/redoc.html', staticAssets); + }); + fastify.get('/api.json', (_request, reply) => { + reply.header('Cache-Control', 'public, max-age=600'); + reply.send(genOpenapiSpec(this.config)); + }); + done(); + } +} diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts new file mode 100644 index 0000000000..fa62480c02 --- /dev/null +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -0,0 +1,193 @@ +import type { Config } from '@/config.js'; +import endpoints from '../endpoints.js'; +import { errors as basicErrors } from './errors.js'; +import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; + +export function genOpenapiSpec(config: Config) { + const spec = { + openapi: '3.0.0', + + info: { + version: config.version, + title: 'Misskey API', + 'x-logo': { url: '/static-assets/api-doc.png' }, + }, + + externalDocs: { + description: 'Repository', + url: 'https://github.com/misskey-dev/misskey', + }, + + servers: [{ + url: config.apiUrl, + }], + + paths: {} as any, + + components: { + schemas: schemas, + + securitySchemes: { + ApiKeyAuth: { + type: 'apiKey', + in: 'body', + name: 'i', + }, + }, + }, + }; + + for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { + const errors = {} as any; + + if (endpoint.meta.errors) { + for (const e of Object.values(endpoint.meta.errors)) { + errors[e.code] = { + value: { + error: e, + }, + }; + } + } + + const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; + + let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; + desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; + if (endpoint.meta.kind) { + const kind = endpoint.meta.kind; + desc += ` / **Permission**: *${kind}*`; + } + + const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; + const schema = { ...endpoint.params }; + + if (endpoint.meta.requireFile) { + schema.properties = { + ...schema.properties, + file: { + type: 'string', + format: 'binary', + description: 'The file contents.', + }, + }; + schema.required = [...schema.required ?? [], 'file']; + } + + const info = { + operationId: endpoint.name, + summary: endpoint.name, + description: desc, + externalDocs: { + description: 'Source code', + url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`, + }, + ...(endpoint.meta.tags ? { + tags: [endpoint.meta.tags[0]], + } : {}), + ...(endpoint.meta.requireCredential ? { + security: [{ + ApiKeyAuth: [], + }], + } : {}), + requestBody: { + required: true, + content: { + [requestType]: { + schema, + }, + }, + }, + responses: { + ...(endpoint.meta.res ? { + '200': { + description: 'OK (with results)', + content: { + 'application/json': { + schema: resSchema, + }, + }, + }, + } : { + '204': { + description: 'OK (without any results)', + }, + }), + '400': { + description: 'Client error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + examples: { ...errors, ...basicErrors['400'] }, + }, + }, + }, + '401': { + description: 'Authentication error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + examples: basicErrors['401'], + }, + }, + }, + '403': { + description: 'Forbidden error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + examples: basicErrors['403'], + }, + }, + }, + '418': { + description: 'I\'m Ai', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + examples: basicErrors['418'], + }, + }, + }, + ...(endpoint.meta.limit ? { + '429': { + description: 'To many requests', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + examples: basicErrors['429'], + }, + }, + }, + } : {}), + '500': { + description: 'Internal server error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + examples: basicErrors['500'], + }, + }, + }, + }, + }; + + spec.paths['/' + endpoint.name] = { + post: info, + }; + } + + return spec; +} diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index 796383f5e3..0cef361caf 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -1,5 +1,5 @@ -import type { Schema } from '@/misc/schema.js'; -import { refs } from '@/misc/schema.js'; +import type { Schema } from '@/misc/json-schema.js'; +import { refs } from '@/misc/json-schema.js'; export function convertSchemaToOpenApiSchema(schema: Schema) { const res: any = schema; diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 3e67880b45..32935325aa 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -27,6 +27,10 @@ export default abstract class Channel { return this.connection.muting; } + protected get renoteMuting() { + return this.connection.renoteMuting; + } + protected get blocking() { return this.connection.blocking; } diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 18604d94f0..e2a42fbfe9 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -39,6 +39,8 @@ class AntennaChannel extends Channel { // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•れã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isUserRelated(note, this.blocking)) return; + if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index f5ef1d1102..12caa7f233 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { isUserRelated } from '@/misc/is-user-related.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; @@ -51,6 +51,8 @@ class ChannelChannel extends Channel { // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•れã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isUserRelated(note, this.blocking)) return; + if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index b8c0076ed9..d79247cd6e 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -68,6 +68,8 @@ class GlobalTimelineChannel extends Channel { // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•れã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isUserRelated(note, this.blocking)) return; + if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã™ã¹ãNoteã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ // TODO: å°†æ¥çš„ã«ã¯ã€å˜ã«MutedNoteテーブルã«ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒã‚ã‚‹ã‹ã©ã†ã‹ã§åˆ¤å®šã—ãŸã„(以下ã®ç†ç”±ã«ã‚ˆã‚Šé›£ã—ãã†ã§ã¯ã‚ã‚‹) // ç¾çжã§ã¯ã€ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã«ãŠã‘ã‚‹MutedNoteレコードã®è¿½åР処ç†ã¯ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã«æµã™å‡¦ç†ã¨ä¸¦åˆ—ã§è¡Œã‚れるãŸã‚〠diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 00f8d8ecd2..98dc858ded 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; @@ -49,6 +49,8 @@ class HashtagChannel extends Channel { if (isUserRelated(note, this.muting)) return; // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•れã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isUserRelated(note, this.blocking)) return; + + if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; this.connection.cacheNote(note); diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 04a9f29686..c623fef64a 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; @@ -75,6 +75,8 @@ class HomeTimelineChannel extends Channel { // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•れã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isUserRelated(note, this.blocking)) return; + if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã™ã¹ãNoteã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ // TODO: å°†æ¥çš„ã«ã¯ã€å˜ã«MutedNoteテーブルã«ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒã‚ã‚‹ã‹ã©ã†ã‹ã§åˆ¤å®šã—ãŸã„(以下ã®ç†ç”±ã«ã‚ˆã‚Šé›£ã—ãã†ã§ã¯ã‚ã‚‹) // ç¾çжã§ã¯ã€ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã«ãŠã‘ã‚‹MutedNoteレコードã®è¿½åР処ç†ã¯ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã«æµã™å‡¦ç†ã¨ä¸¦åˆ—ã§è¡Œã‚れるãŸã‚〠diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index ab52aabb30..f54767bc9d 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -86,6 +86,8 @@ class HybridTimelineChannel extends Channel { // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•れã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isUserRelated(note, this.blocking)) return; + if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã™ã¹ãNoteã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ // TODO: å°†æ¥çš„ã«ã¯ã€å˜ã«MutedNoteテーブルã«ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒã‚ã‚‹ã‹ã©ã†ã‹ã§åˆ¤å®šã—ãŸã„(以下ã®ç†ç”±ã«ã‚ˆã‚Šé›£ã—ãã†ã§ã¯ã‚ã‚‹) // ç¾çжã§ã¯ã€ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã«ãŠã‘ã‚‹MutedNoteレコードã®è¿½åР処ç†ã¯ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã«æµã™å‡¦ç†ã¨ä¸¦åˆ—ã§è¡Œã‚れるãŸã‚〠diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index d8532c477b..eb0642900d 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -65,6 +65,8 @@ class LocalTimelineChannel extends Channel { // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•れã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isUserRelated(note, this.blocking)) return; + if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + // æµã‚Œã¦ããŸNoteãŒãƒŸãƒ¥ãƒ¼ãƒˆã™ã¹ãNoteã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ // TODO: å°†æ¥çš„ã«ã¯ã€å˜ã«MutedNoteテーブルã«ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒã‚ã‚‹ã‹ã©ã†ã‹ã§åˆ¤å®šã—ãŸã„(以下ã®ç†ç”±ã«ã‚ˆã‚Šé›£ã—ãã†ã§ã¯ã‚ã‚‹) // ç¾çжã§ã¯ã€ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã«ãŠã‘ã‚‹MutedNoteレコードã®è¿½åР処ç†ã¯ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã«æµã™å‡¦ç†ã¨ä¸¦åˆ—ã§è¡Œã‚れるãŸã‚〠diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 7254d0a6d4..8a42e99a54 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -93,6 +93,8 @@ class UserListChannel extends Channel { // æµã‚Œã¦ããŸNoteãŒãƒ–ãƒãƒƒã‚¯ã•れã¦ã„るユーザーãŒé–¢ã‚ã‚‹ã‚‚ã®ã ã£ãŸã‚‰ç„¡è¦–ã™ã‚‹ if (isUserRelated(note, this.blocking)) return; + if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return; + this.send('note', note); } diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index d3056aca57..7c6eb9a20a 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -1,9 +1,9 @@ import type { User } from '@/models/entities/User.js'; import type { Channel as ChannelModel } from '@/models/entities/Channel.js'; -import type { FollowingsRepository, MutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js'; +import type { FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import type { UserProfile } from '@/models/entities/UserProfile.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { NoteReadService } from '@/core/NoteReadService.js'; import type { NotificationService } from '@/core/NotificationService.js'; @@ -22,6 +22,7 @@ export default class Connection { public userProfile?: UserProfile | null; public following: Set<User['id']> = new Set(); public muting: Set<User['id']> = new Set(); + public renoteMuting: Set<User['id']> = new Set(); public blocking: Set<User['id']> = new Set(); // "被"blocking public followingChannels: Set<ChannelModel['id']> = new Set(); public token?: AccessToken; @@ -34,6 +35,7 @@ export default class Connection { constructor( private followingsRepository: FollowingsRepository, private mutingsRepository: MutingsRepository, + private renoteMutingsRepository: RenoteMutingsRepository, private blockingsRepository: BlockingsRepository, private channelFollowingsRepository: ChannelFollowingsRepository, private userProfilesRepository: UserProfilesRepository, @@ -66,6 +68,7 @@ export default class Connection { if (this.user) { this.updateFollowing(); this.updateMuting(); + this.updateRenoteMuting(); this.updateBlocking(); this.updateFollowingChannels(); this.updateUserProfile(); @@ -93,6 +96,7 @@ export default class Connection { this.muting.delete(data.body.id); break; + // TODO: renote mute events // TODO: block events case 'followChannel': @@ -343,6 +347,18 @@ export default class Connection { } @bindThis + private async updateRenoteMuting() { + const renoteMutings = await this.renoteMutingsRepository.find({ + where: { + muterId: this.user!.id, + }, + select: ['muteeId'], + }); + + this.renoteMuting = new Set<string>(renoteMutings.map(x => x.muteeId)); + } + + @bindThis private async updateBlocking() { // ã“ã“ã§ã„ã†Blockingã¯è¢«Blockingã®æ„ const blockings = await this.blockingsRepository.find({ where: { diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index c450773055..b8f50e0546 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -9,7 +9,7 @@ import type { UserList } from '@/models/entities/UserList.js'; import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; import type { Signin } from '@/models/entities/Signin.js'; import type { Page } from '@/models/entities/Page.js'; -import type { Packed } from '@/misc/schema.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { Webhook } from '@/models/entities/Webhook.js'; import type { Meta } from '@/models/entities/Meta.js'; import { Role, RoleAssignment } from '@/models'; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 98cdd31206..fb76f07e48 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -195,11 +195,6 @@ export class ClientServerService { //#region static assets fastify.register(fastifyStatic, { - root: _dirname, - serve: false, - }); - - fastify.register(fastifyStatic, { root: staticAssets, prefix: '/static-assets/', maxAge: ms('7 days'), diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 2ce7293a52..b3e193cd34 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { summaly } from 'summaly'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; @@ -9,6 +8,7 @@ import type Logger from '@/logger.js'; import { query } from '@/misc/prelude/url.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; +import { ApiError } from '@/server/api/error.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -19,9 +19,6 @@ export class UrlPreviewService { @Inject(DI.config) private config: Config, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - private metaService: MetaService, private httpRequestService: HttpRequestService, private loggerService: LoggerService, @@ -43,23 +40,23 @@ export class UrlPreviewService { @bindThis public async handle( - request: FastifyRequest<{ Querystring: { url: string; lang: string; } }>, + request: FastifyRequest<{ Querystring: { url: string; lang?: string; } }>, reply: FastifyReply, - ) { + ): Promise<object | undefined> { const url = request.query.url; if (typeof url !== 'string') { reply.code(400); return; } - + const lang = request.query.lang; if (Array.isArray(lang)) { reply.code(400); return; } - + const meta = await this.metaService.fetch(); - + this.logger.info(meta.summalyProxy ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`); @@ -81,26 +78,32 @@ export class UrlPreviewService { this.logger.succ(`Got preview of ${url}: ${summary.title}`); - if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { + if (!(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { throw new Error('unsupported schema included'); } - if (summary.player?.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) { + if (summary.player.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) { throw new Error('unsupported schema included'); } - + summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); - + // Cache 7days reply.header('Cache-Control', 'max-age=604800, immutable'); - + return summary; } catch (err) { this.logger.warn(`Failed to get preview of ${url}: ${err}`); - reply.code(200); + reply.code(422); reply.header('Cache-Control', 'max-age=86400, immutable'); - return {}; + return { + error: new ApiError({ + message: 'Failed to get preview', + code: 'URL_PREVIEW_FAILED', + id: '09d01cb5-53b9-4856-82e5-38a50c290a3b', + }), + }; } } } diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 8d6897c46d..a9a0dfd4ee 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -36,7 +36,7 @@ html link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') //- https://github.com/misskey-dev/misskey/issues/9842 - link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.2.0') + link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.10.0') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) if !config.clientManifestExists diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts new file mode 100644 index 0000000000..0addb430c9 --- /dev/null +++ b/packages/backend/test/e2e/2fa.ts @@ -0,0 +1,439 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as crypto from 'node:crypto'; +import * as cbor from 'cbor'; +import * as OTPAuth from 'otpauth'; +import { loadConfig } from '../../src/config.js'; +import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('2è¦ç´ èªè¨¼', () => { + let app: INestApplicationContext; + let alice: unknown; + + const config = loadConfig(); + const password = 'test'; + const username = 'alice'; + + // https://datatracker.ietf.org/doc/html/rfc8152 + // å„値ã®å®šç¾©ã¯ä¸Šè¨˜è¦æ ¼ã«åŸºã¥ã。éµãƒšã‚¢ã¯é©å½“ã«ç”Ÿæˆã—ãŸã‚„㤠+ const coseKtyEc2 = 2; + const coseKid = 'meriadoc.brandybuck@buckland.example'; + const coseAlgEs256 = -7; + const coseEc2CrvP256 = 1; + const coseEc2X = '4932eaacc657565705e4287e7870ce3aad55545d99d35a98a472dc52880cfc8f'; + const coseEc2Y = '5ca68303bf2c0433473e3d5cb8586bc2c8c43a4945a496fce8dbeda8b23ab0b1'; + + // private key only for testing + const pemToSign = '-----BEGIN EC PRIVATE KEY-----\n' + + 'MHcCAQEEIHqe/keuXyolbXzgLOu+YFJjDBGWVgXc3QCXfyqwDPf2oAoGCCqGSM49\n' + + 'AwEHoUQDQgAESTLqrMZXVlcF5Ch+eHDOOq1VVF2Z01qYpHLcUogM/I9cpoMDvywE\n' + + 'M0c+PVy4WGvCyMQ6SUWklvzo2+2osjqwsQ==\n' + + '-----END EC PRIVATE KEY-----\n'; + + const otpToken = (secret: string): string => { + return OTPAuth.TOTP.generate({ + secret: OTPAuth.Secret.fromBase32(secret), + digits: 6, + }); + }; + + const rpIdHash = (): Buffer => { + return crypto.createHash('sha256') + .update(Buffer.from(config.hostname, 'utf-8')) + .digest(); + }; + + const keyDoneParam = (param: { + keyName: string, + challengeId: string, + challenge: string, + credentialId: Buffer, + }): { + attestationObject: string, + challengeId: string, + clientDataJSON: string, + password: string, + name: string, + } => { + // A COSE encoded public key + const credentialPublicKey = cbor.encode(new Map<number, unknown>([ + [-1, coseEc2CrvP256], + [-2, Buffer.from(coseEc2X, 'hex')], + [-3, Buffer.from(coseEc2Y, 'hex')], + [1, coseKtyEc2], + [2, coseKid], + [3, coseAlgEs256], + ])); + + // AuthenticatorAssertionResponse.authenticatorData + // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData + const credentialIdLength = Buffer.allocUnsafe(2); + credentialIdLength.writeUInt16BE(param.credentialId.length); + const authData = Buffer.concat([ + rpIdHash(), // rpIdHash(32) + Buffer.from([0x45]), // flags(1) + Buffer.from([0x00, 0x00, 0x00, 0x00]), // signCount(4) + Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), // AAGUID(16) + credentialIdLength, + param.credentialId, + credentialPublicKey, + ]); + + return { + attestationObject: cbor.encode({ + fmt: 'none', + attStmt: {}, + authData, + }).toString('hex'), + challengeId: param.challengeId, + clientDataJSON: JSON.stringify({ + type: 'webauthn.create', + challenge: param.challenge, + origin: config.scheme + '://' + config.host, + androidPackageName: 'org.mozilla.firefox', + }), + password, + name: param.keyName, + }; + }; + + const signinParam = (): { + username: string, + password: string, + 'g-recaptcha-response'?: string | null, + 'hcaptcha-response'?: string | null, + } => { + return { + username, + password, + 'g-recaptcha-response': null, + 'hcaptcha-response': null, + }; + }; + + const signinWithSecurityKeyParam = (param: { + keyName: string, + challengeId: string, + challenge: string, + credentialId: Buffer, + }): { + authenticatorData: string, + credentialId: string, + challengeId: string, + clientDataJSON: string, + username: string, + password: string, + signature: string, + 'g-recaptcha-response'?: string | null, + 'hcaptcha-response'?: string | null, + } => { + // AuthenticatorAssertionResponse.authenticatorData + // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData + const authenticatorData = Buffer.concat([ + rpIdHash(), + Buffer.from([0x05]), // flags(1) + Buffer.from([0x00, 0x00, 0x00, 0x01]), // signCount(4) + ]); + const clientDataJSONBuffer = Buffer.from(JSON.stringify({ + type: 'webauthn.get', + challenge: param.challenge, + origin: config.scheme + '://' + config.host, + androidPackageName: 'org.mozilla.firefox', + })); + const hashedclientDataJSON = crypto.createHash('sha256') + .update(clientDataJSONBuffer) + .digest(); + const privateKey = crypto.createPrivateKey(pemToSign); + const signature = crypto.createSign('SHA256') + .update(Buffer.concat([authenticatorData, hashedclientDataJSON])) + .sign(privateKey); + return { + authenticatorData: authenticatorData.toString('hex'), + credentialId: param.credentialId.toString('base64'), + challengeId: param.challengeId, + clientDataJSON: clientDataJSONBuffer.toString('hex'), + username, + password, + signature: signature.toString('hex'), + 'g-recaptcha-response': null, + 'hcaptcha-response': null, + }; + }; + + beforeAll(async () => { + app = await startServer(); + alice = await signup({ username, password }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + test('ãŒè¨å®šã§ãã€OTPã§ãƒã‚°ã‚¤ãƒ³ã§ãる。', async () => { + const registerResponse = await api('/i/2fa/register', { + password, + }, alice); + assert.strictEqual(registerResponse.status, 200); + assert.notEqual(registerResponse.body.qr, undefined); + assert.notEqual(registerResponse.body.url, undefined); + assert.notEqual(registerResponse.body.secret, undefined); + assert.strictEqual(registerResponse.body.label, username); + assert.strictEqual(registerResponse.body.issuer, config.host); + + const doneResponse = await api('/i/2fa/done', { + token: otpToken(registerResponse.body.secret), + }, alice); + assert.strictEqual(doneResponse.status, 204); + + const usersShowResponse = await api('/users/show', { + username, + }, alice); + assert.strictEqual(usersShowResponse.status, 200); + assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); + + const signinResponse = await api('/signin', { + ...signinParam(), + token: otpToken(registerResponse.body.secret), + }); + assert.strictEqual(signinResponse.status, 200); + assert.notEqual(signinResponse.body.i, undefined); + }); + + test('ãŒè¨å®šã§ãã€ã‚»ã‚ュリティã‚ーã§ãƒã‚°ã‚¤ãƒ³ã§ãる。', async () => { + const registerResponse = await api('/i/2fa/register', { + password, + }, alice); + assert.strictEqual(registerResponse.status, 200); + + const doneResponse = await api('/i/2fa/done', { + token: otpToken(registerResponse.body.secret), + }, alice); + assert.strictEqual(doneResponse.status, 204); + + const registerKeyResponse = await api('/i/2fa/register-key', { + password, + }, alice); + assert.strictEqual(registerKeyResponse.status, 200); + assert.notEqual(registerKeyResponse.body.challengeId, undefined); + assert.notEqual(registerKeyResponse.body.challenge, undefined); + + const keyName = 'example-key'; + const credentialId = crypto.randomBytes(0x41); + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ + keyName, + challengeId: registerKeyResponse.body.challengeId, + challenge: registerKeyResponse.body.challenge, + credentialId, + }), alice); + assert.strictEqual(keyDoneResponse.status, 200); + assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('hex')); + assert.strictEqual(keyDoneResponse.body.name, keyName); + + const usersShowResponse = await api('/users/show', { + username, + }); + assert.strictEqual(usersShowResponse.status, 200); + assert.strictEqual(usersShowResponse.body.securityKeys, true); + + const signinResponse = await api('/signin', { + ...signinParam(), + }); + assert.strictEqual(signinResponse.status, 200); + assert.strictEqual(signinResponse.body.i, undefined); + assert.notEqual(signinResponse.body.challengeId, undefined); + assert.notEqual(signinResponse.body.challenge, undefined); + assert.notEqual(signinResponse.body.securityKeys, undefined); + assert.strictEqual(signinResponse.body.securityKeys[0].id, credentialId.toString('hex')); + + const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({ + keyName, + challengeId: signinResponse.body.challengeId, + challenge: signinResponse.body.challenge, + credentialId, + })); + assert.strictEqual(signinResponse2.status, 200); + assert.notEqual(signinResponse2.body.i, undefined); + }); + + test('ãŒè¨å®šã§ãã€ã‚»ã‚ュリティã‚ーã§ãƒ‘スワードレスãƒã‚°ã‚¤ãƒ³ã§ãる。', async () => { + const registerResponse = await api('/i/2fa/register', { + password, + }, alice); + assert.strictEqual(registerResponse.status, 200); + + const doneResponse = await api('/i/2fa/done', { + token: otpToken(registerResponse.body.secret), + }, alice); + assert.strictEqual(doneResponse.status, 204); + + const registerKeyResponse = await api('/i/2fa/register-key', { + password, + }, alice); + assert.strictEqual(registerKeyResponse.status, 200); + + const keyName = 'example-key'; + const credentialId = crypto.randomBytes(0x41); + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ + keyName, + challengeId: registerKeyResponse.body.challengeId, + challenge: registerKeyResponse.body.challenge, + credentialId, + }), alice); + assert.strictEqual(keyDoneResponse.status, 200); + + const passwordLessResponse = await api('/i/2fa/password-less', { + value: true, + }, alice); + assert.strictEqual(passwordLessResponse.status, 204); + + const usersShowResponse = await api('/users/show', { + username, + }); + assert.strictEqual(usersShowResponse.status, 200); + assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true); + + const signinResponse = await api('/signin', { + ...signinParam(), + password: '', + }); + assert.strictEqual(signinResponse.status, 200); + assert.strictEqual(signinResponse.body.i, undefined); + + const signinResponse2 = await api('/signin', { + ...signinWithSecurityKeyParam({ + keyName, + challengeId: signinResponse.body.challengeId, + challenge: signinResponse.body.challenge, + credentialId, + }), + password: '', + }); + assert.strictEqual(signinResponse2.status, 200); + assert.notEqual(signinResponse2.body.i, undefined); + }); + + test('ãŒè¨å®šã§ãã€è¨å®šã—ãŸã‚»ã‚ュリティã‚ーã®åå‰ã‚’変更ã§ãる。', async () => { + const registerResponse = await api('/i/2fa/register', { + password, + }, alice); + assert.strictEqual(registerResponse.status, 200); + + const doneResponse = await api('/i/2fa/done', { + token: otpToken(registerResponse.body.secret), + }, alice); + assert.strictEqual(doneResponse.status, 204); + + const registerKeyResponse = await api('/i/2fa/register-key', { + password, + }, alice); + assert.strictEqual(registerKeyResponse.status, 200); + + const keyName = 'example-key'; + const credentialId = crypto.randomBytes(0x41); + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ + keyName, + challengeId: registerKeyResponse.body.challengeId, + challenge: registerKeyResponse.body.challenge, + credentialId, + }), alice); + assert.strictEqual(keyDoneResponse.status, 200); + + const renamedKey = 'other-key'; + const updateKeyResponse = await api('/i/2fa/update-key', { + name: renamedKey, + credentialId: credentialId.toString('hex'), + }, alice); + assert.strictEqual(updateKeyResponse.status, 200); + + const iResponse = await api('/i', { + }, alice); + assert.strictEqual(iResponse.status, 200); + const securityKeys = iResponse.body.securityKeysList.filter(s => s.id === credentialId.toString('hex')); + assert.strictEqual(securityKeys.length, 1); + assert.strictEqual(securityKeys[0].name, renamedKey); + assert.notEqual(securityKeys[0].lastUsed, undefined); + }); + + test('ãŒè¨å®šã§ãã€è¨å®šã—ãŸã‚»ã‚ュリティã‚ーを削除ã§ãる。', async () => { + const registerResponse = await api('/i/2fa/register', { + password, + }, alice); + assert.strictEqual(registerResponse.status, 200); + + const doneResponse = await api('/i/2fa/done', { + token: otpToken(registerResponse.body.secret), + }, alice); + assert.strictEqual(doneResponse.status, 204); + + const registerKeyResponse = await api('/i/2fa/register-key', { + password, + }, alice); + assert.strictEqual(registerKeyResponse.status, 200); + + const keyName = 'example-key'; + const credentialId = crypto.randomBytes(0x41); + const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ + keyName, + challengeId: registerKeyResponse.body.challengeId, + challenge: registerKeyResponse.body.challenge, + credentialId, + }), alice); + assert.strictEqual(keyDoneResponse.status, 200); + + // テストã®å®Ÿè¡Œé †ã«ã‚ˆã£ã¦ã¯è¤‡æ•°æ®‹ã£ã¦ã‚‹ã®ã§å…¨éƒ¨æ¶ˆã™ + 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', { + password, + credentialId: key.id, + }, alice); + assert.strictEqual(removeKeyResponse.status, 200); + } + + const usersShowResponse = await api('/users/show', { + username, + }); + assert.strictEqual(usersShowResponse.status, 200); + assert.strictEqual(usersShowResponse.body.securityKeys, false); + + const signinResponse = await api('/signin', { + ...signinParam(), + token: otpToken(registerResponse.body.secret), + }); + assert.strictEqual(signinResponse.status, 200); + assert.notEqual(signinResponse.body.i, undefined); + }); + + test('ãŒè¨å®šã§ãã€è¨å®šè§£é™¤ã§ãる。(パスワードã®ã¿ã§ãƒã‚°ã‚¤ãƒ³ã§ãる。)', async () => { + const registerResponse = await api('/i/2fa/register', { + password, + }, alice); + assert.strictEqual(registerResponse.status, 200); + + const doneResponse = await api('/i/2fa/done', { + token: otpToken(registerResponse.body.secret), + }, alice); + assert.strictEqual(doneResponse.status, 204); + + 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', { + password, + }, alice); + assert.strictEqual(unregisterResponse.status, 204); + + const signinResponse = await api('/signin', { + ...signinParam(), + }); + assert.strictEqual(signinResponse.status, 200); + assert.notEqual(signinResponse.body.i, undefined); + }); +}); diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts index 4e162f42d0..3af0d35182 100644 --- a/packages/backend/test/e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -5,14 +5,14 @@ import { signup, api, post, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; describe('API visibility', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; beforeAll(async () => { - p = await startServer(); + app = await startServer(); }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); describe('Note visibility', () => { diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index 6ceccf66eb..a46f336a70 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -5,20 +5,20 @@ import { signup, api, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; describe('API', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; let alice: any; let bob: any; let carol: any; beforeAll(async () => { - p = await startServer(); + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); describe('General validation', () => { diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts index 4e9030f85d..57a46ab38a 100644 --- a/packages/backend/test/e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -5,7 +5,7 @@ import { signup, api, post, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; describe('Block', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; // alice blocks bob let alice: any; @@ -13,14 +13,14 @@ describe('Block', () => { let carol: any; beforeAll(async () => { - p = await startServer(); + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); test('Block作æˆ', async () => { @@ -70,9 +70,9 @@ describe('Block', () => { // TODO: ユーザーリストã‹ã‚‰é™¤å¤–ã•れるテスト test('タイムライン(LTL)ã«ãƒ–ãƒãƒƒã‚¯ã•れã¦ã„ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { - const aliceNote = await post(alice); - const bobNote = await post(bob); - const carolNote = await post(carol); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi' }); + const carolNote = await post(carol, { text: 'hi' }); const res = await api('/notes/local-timeline', {}, bob); diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts new file mode 100644 index 0000000000..f35aae9dc6 --- /dev/null +++ b/packages/backend/test/e2e/clips.ts @@ -0,0 +1,962 @@ +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 { + signup, + post, + startServer, + api, + successfulApiCall, + failedApiCall, + ApiRequest, + hiddenNote, +} from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('クリップ', () => { + type User = Packed<'User'>; + type Note = Packed<'Note'>; + type Clip = Packed<'Clip'>; + + let app: INestApplicationContext; + + 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; + + 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> => ({ + name: 'test', + }); + const create = async (parameters: Partial<CreateParam> = {}, request: Partial<ApiRequest> = {}): Promise<Clip> => { + const clip = await successfulApiCall<Clip>({ + endpoint: '/clips/create', + parameters: { + ...defaultCreate(), + ...parameters, + }, + user: alice, + ...request, + }); + + // 入力ãŒçµæžœã¨ã—ã¦å…¥ã£ã¦ã„ã‚‹ã“㨠+ assert.deepStrictEqual(clip, { + ...clip, + ...defaultCreate(), + ...parameters, + }); + return clip; + }; + + const createMany = async (parameters: Partial<CreateParam>, count = 10, user = alice): Promise<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', + parameters: { + name: 'updated', + ...parameters, + }, + user: alice, + ...request, + }); + + // 入力ãŒçµæžœã¨ã—ã¦å…¥ã£ã¦ã„ã‚‹ã“ã¨ã€‚clipIdã¯idã«ãªã‚‹ã®ã§æ¶ˆã—ã¦ãŠã + delete (parameters as { clipId?: string }).clipId; + assert.deepStrictEqual(clip, { + ...clip, + ...parameters, + }); + return clip; + }; + + type DeleteParam = JTDDataType<typeof DeleteParamDef>; + const deleteClip = async (parameters: DeleteParam, request: Partial<ApiRequest> = {}): Promise<void> => { + return await successfulApiCall<void>({ + endpoint: '/clips/delete', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type ShowParam = JTDDataType<typeof ShowParamDef>; + const show = async (parameters: ShowParam, request: Partial<ApiRequest> = {}): Promise<Clip> => { + return await successfulApiCall<Clip>({ + endpoint: '/clips/show', + parameters, + user: alice, + ...request, + }); + }; + + const list = async (request: Partial<ApiRequest>): Promise<Clip[]> => { + return successfulApiCall<Clip[]>({ + endpoint: '/clips/list', + parameters: {}, + user: alice, + ...request, + }); + }; + + const usersClips = async (request: Partial<ApiRequest>): Promise<Clip[]> => { + return await successfulApiCall<Clip[]>({ + endpoint: '/users/clips', + parameters: {}, + user: alice, + ...request, + }); + }; + + beforeAll(async () => { + app = await startServer(); + 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; + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + afterEach(async () => { + // テスト間ã§å½±éŸ¿ã—åˆã‚ãªã„よã†ã«æ¯Žå›žå…¨éƒ¨æ¶ˆã™ã€‚ + for (const user of [alice, bob]) { + const list = await api('/clips/list', { limit: 11 }, user); + for (const clip of list.body) { + await api('/clips/delete', { clipId: clip.id }, user); + } + } + }); + + test('ã®ä½œæˆãŒã§ãã‚‹', async () => { + const res = await create(); + // ISO 8601ã§æ—¥ä»˜ãŒè¿”ã£ã¦ãã‚‹ã“㨠+ assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.lastClippedAt, null); + assert.strictEqual(res.name, 'test'); + assert.strictEqual(res.description, null); + assert.strictEqual(res.isPublic, false); + assert.strictEqual(res.favoritedCount, 0); + assert.strictEqual(res.isFavorited, false); + }); + + test('ã®ä½œæˆã¯ãƒãƒªã‚·ãƒ¼ã§å®šã‚ã‚‰ã‚ŒãŸæ•°ä»¥ä¸Šã¯ã§ããªã„。', async () => { + // ãƒãƒªã‚·ãƒ¼ + 1ã¾ã§ä½œã‚Œã‚‹ã¨ã„ã†æ‰€ãŒãƒŸã‚½ + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; + for (let i = 0; i < clipLimit; i++) { + await create(); + } + + await failedApiCall({ + endpoint: '/clips/create', + parameters: defaultCreate(), + user: alice, + }, { + status: 400, + code: 'TOO_MANY_CLIPS', + id: '920f7c2d-6208-4b76-8082-e632020f5883', + }); + }); + + const createClipAllowedPattern = [ + { label: 'nameãŒæœ€å¤§é•·', parameters: { name: 'x'.repeat(100) } }, + { label: 'private', parameters: { isPublic: false } }, + { label: 'public', parameters: { isPublic: true } }, + { label: 'descriptionãŒnull', parameters: { description: null } }, + { label: 'descriptionãŒæœ€å¤§é•·', parameters: { description: 'a'.repeat(2048) } }, + ]; + test.each(createClipAllowedPattern)('ã®ä½œæˆã¯$labelã§ã‚‚ã§ãã‚‹', async ({ parameters }) => await create(parameters)); + + const createClipDenyPattern = [ + { label: 'nameãŒnull', parameters: { name: null } }, + { label: 'nameãŒæœ€å¤§é•·+1', parameters: { name: 'x'.repeat(101) } }, + { label: 'isPublicãŒboolã˜ã‚ƒãªã„', parameters: { isPublic: 'true' } }, + { label: 'descriptionãŒã‚¼ãƒé•·', parameters: { description: '' } }, + { label: 'descriptionãŒæœ€å¤§é•·+1', parameters: { description: 'a'.repeat(2049) } }, + ]; + test.each(createClipDenyPattern)('ã®ä½œæˆã¯$labelãªã‚‰ã§ããªã„', async ({ parameters }) => failedApiCall({ + endpoint: '/clips/create', + parameters: { + ...defaultCreate(), + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + })); + + test('ã®æ›´æ–°ãŒã§ãã‚‹', async () => { + const res = await update({ + clipId: (await create()).id, + name: 'updated', + description: 'new description', + isPublic: true, + }); + + // ISO 8601ã§æ—¥ä»˜ãŒè¿”ã£ã¦ãã‚‹ã“㨠+ assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.lastClippedAt, null); + assert.strictEqual(res.name, 'updated'); + assert.strictEqual(res.description, 'new description'); + assert.strictEqual(res.isPublic, true); + assert.strictEqual(res.favoritedCount, 0); + assert.strictEqual(res.isFavorited, false); + }); + + test.each(createClipAllowedPattern)('ã®æ›´æ–°ã¯$labelã§ã‚‚ã§ãã‚‹', async ({ parameters }) => await update({ + clipId: (await create()).id, + name: 'updated', + ...parameters, + })); + + test.each([ + { label: 'clipIdãŒnull', parameters: { clipId: null } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): 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', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + name: 'updated', + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('ã®å‰Šé™¤ãŒã§ãã‚‹', async () => { + await deleteClip({ + clipId: (await create()).id, + }); + assert.deepStrictEqual(await list({}), []); + }); + + test.each([ + { label: 'clipIdãŒnull', parameters: { clipId: null } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '70ca08ba-6865-4630-b6fb-8494759aa754', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): User => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: '70ca08ba-6865-4630-b6fb-8494759aa754', + } }, + ])('ã®å‰Šé™¤ã¯$labelãªã‚‰ã§ããªã„', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/delete', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('ã®ID指定å–å¾—ãŒã§ãã‚‹', async () => { + const clip = await create(); + const res = await show({ clipId: clip.id }); + assert.deepStrictEqual(res, clip); + }); + + test('ã®ID指定å–å¾—ã¯ä»–人ã®Privateãªã‚¯ãƒªãƒƒãƒ—ã¯å–å¾—ã§ããªã„', async () => { + const clip = await create({ isPublic: false }, { user: bob } ); + failedApiCall({ + endpoint: '/clips/show', + parameters: { clipId: clip.id }, + user: alice, + }, { + status: 400, + code: 'NO_SUCH_CLIP', + id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', + }); + }); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_CLIP', + id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', + } }, + ])('ã®ID指定å–å¾—ã¯$labelãªã‚‰ã§ããªã„', async ({ parameters, assetion }) => failedApiCall({ + endpoint: '/clips/show', + parameters: { + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assetion, + })); + + test('ã®ä¸€è¦§(clips/list)ãŒå–å¾—ã§ãã‚‹(空)', async () => { + const res = await list({}); + assert.deepStrictEqual(res, []); + }); + + test('ã®ä¸€è¦§(clips/list)ãŒå–å¾—ã§ãã‚‹(上é™ã„ã£ã±ã„)', async () => { + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; + const clips = await createMany({}, clipLimit); + const res = await list({ + parameters: { limit: 1 }, // FIXME: 無視ã•れã¦11全部返ã£ã¦ãã‚‹ + }); + + // è¿”ã£ã¦ãã‚‹é…列ã«ã¯é †åºä¿éšœãŒãªã„ã®ã§idã§ã‚½ãƒ¼ãƒˆã—ã¦åŽ³å¯†æ¯”è¼ƒ + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + clips.sort(compareBy(s => s.id)), + ); + }); + + test('ã®ä¸€è¦§ãŒå–å¾—ã§ãã‚‹(空)', async () => { + const res = await usersClips({ + parameters: { + userId: alice.id, + }, + }); + assert.deepStrictEqual(res, []); + }); + + test.each([ + { label: '' }, + { label: '他人アカウントã‹ã‚‰', user: (): 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)), + clips.sort(compareBy(s => s.id))); + + // èªè¨¼çŠ¶æ…‹ã§è¦‹ãŸã¨ãã ã‘isFavoritedãŒå…¥ã£ã¦ã„ã‚‹ + for (const clip of res) { + assert.strictEqual(clip.isFavorited, false); + } + }); + + test.each([ + { label: '未èªè¨¼', user: (): undefined => 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))(), + }); + + // 未èªè¨¼ã§è¦‹ãŸã¨ãã¯isFavoritedã¯å…¥ã‚‰ãªã„ + for (const clip of res) { + assert.strictEqual('isFavorited' in clip, false); + } + }); + + test('ã®ä¸€è¦§ã¯Privateãªã‚¯ãƒªãƒƒãƒ—ã‚’å«ã¾ãªã„(自分ã®ã‚‚ã®ã§ã‚ã£ã¦ã‚‚。)', async () => { + await create({ isPublic: false }); + const aliceClip = await create({ isPublic: true }); + const res = await usersClips({ + parameters: { + userId: alice.id, + limit: 2, + }, + }); + assert.deepStrictEqual(res, [aliceClip]); + }); + + test('ã®ä¸€è¦§ã¯ID指定ã§ç¯„å›²é¸æŠžãŒã§ãã‚‹', async () => { + 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)), + [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)); + }); + + test.each([ + { label: 'userId未指定', parameters: { userId: undefined } }, + { label: 'limitゼãƒ', parameters: { limit: 0 } }, + { label: 'limit最大+1', parameters: { limit: 101 } }, + ])('ã®ä¸€è¦§ã¯$labelã ã¨å–å¾—ã§ããªã„', async ({ parameters }) => failedApiCall({ + endpoint: '/users/clips', + parameters: { + userId: alice.id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + })); + + 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ã¯æœªèªè¨¼ã§ã¯ã§ããªã„', async ({ endpoint }) => await failedApiCall({ + endpoint: endpoint, + parameters: {}, + user: undefined, + }, { + status: 401, + code: 'CREDENTIAL_REQUIRED', + id: '1384574d-a912-4b81-8601-c7b1c4085df1', + })); + + describe('ã®ãŠæ°—ã«å…¥ã‚Š', () => { + let aliceClip: Clip; + + type FavoriteParam = JTDDataType<typeof FavoriteParamDef>; + const favorite = async (parameters: FavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => { + return successfulApiCall<void>({ + endpoint: '/clips/favorite', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type UnfavoriteParam = JTDDataType<typeof UnfavoriteParamDef>; + const unfavorite = async (parameters: UnfavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => { + return successfulApiCall<void>({ + endpoint: '/clips/unfavorite', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + const myFavorites = async (request: Partial<ApiRequest> = {}): Promise<Clip[]> => { + return successfulApiCall<Clip[]>({ + endpoint: '/clips/my-favorites', + parameters: {}, + user: alice, + ...request, + }); + }; + + beforeEach(async () => { + aliceClip = await create(); + }); + + test('ã‚’è¨å®šã§ãる。', async () => { + await favorite({ clipId: aliceClip.id }); + const clip = await show({ clipId: aliceClip.id }); + assert.strictEqual(clip.favoritedCount, 1); + assert.strictEqual(clip.isFavorited, true); + }); + + test('ã¯Publicãªä»–人ã®ã‚¯ãƒªãƒƒãƒ—ã«è¨å®šã§ãる。', async () => { + const publicClip = await create({ isPublic: true }); + await favorite({ clipId: publicClip.id }, { user: bob }); + const clip = await show({ clipId: publicClip.id }, { user: bob }); + assert.strictEqual(clip.favoritedCount, 1); + assert.strictEqual(clip.isFavorited, true); + + // isFavoritedã¯è¦‹ã‚‹äººã«ã‚ˆã£ã¦åˆ‡ã‚Šæ›¿ã‚る。 + const clip2 = await show({ clipId: publicClip.id }); + assert.strictEqual(clip2.favoritedCount, 1); + assert.strictEqual(clip2.isFavorited, false); + }); + + test('ã¯1ã¤ã®ã‚¯ãƒªãƒƒãƒ—ã«å¯¾ã—ã¦è¤‡æ•°äººãŒè¨å®šã§ãる。', async () => { + const publicClip = await create({ isPublic: true }); + await favorite({ clipId: publicClip.id }, { user: bob }); + await favorite({ clipId: publicClip.id }); + const clip = await show({ clipId: publicClip.id }, { user: bob }); + assert.strictEqual(clip.favoritedCount, 2); + assert.strictEqual(clip.isFavorited, true); + + const clip2 = await show({ clipId: publicClip.id }); + assert.strictEqual(clip2.favoritedCount, 2); + assert.strictEqual(clip2.isFavorited, true); + }); + + test('ã¯11ã‚’è¶…ãˆã¦è¨å®šã§ãる。', async () => { + const clips = [ + aliceClip, + ...await createMany({}, 10, alice), + ...await createMany({ isPublic: true }, 10, bob), + ]; + for (const clip of clips) { + await favorite({ clipId: clip.id }); + } + + // pagenationã¯ãªã„。全部一気ã«ã¨ã‚Œã‚‹ã€‚ + const favorited = await myFavorites(); + assert.strictEqual(favorited.length, clips.length); + for (const clip of favorited) { + assert.strictEqual(clip.favoritedCount, 1); + assert.strictEqual(clip.isFavorited, true); + } + }); + + test('ã¯åŒã˜ã‚¯ãƒªãƒƒãƒ—ã«å¯¾ã—ã¦äºŒå›žè¨å®šã§ããªã„。', async () => { + await favorite({ clipId: aliceClip.id }); + await failedApiCall({ + endpoint: '/clips/favorite', + parameters: { + clipId: aliceClip.id, + }, + user: alice, + }, { + status: 400, + code: 'ALREADY_FAVORITED', + id: '92658936-c625-4273-8326-2d790129256e', + }); + }); + + test.each([ + { label: 'clipIdãŒnull', parameters: { clipId: null } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): User => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', + } }, + ])('ã®è¨å®šã¯$labelãªã‚‰ã§ããªã„', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/favorite', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('ã‚’è¨å®šè§£é™¤ã§ãる。', async () => { + await favorite({ clipId: aliceClip.id }); + await unfavorite({ clipId: aliceClip.id }); + const clip = await show({ clipId: aliceClip.id }); + assert.strictEqual(clip.favoritedCount, 0); + assert.strictEqual(clip.isFavorited, false); + assert.deepStrictEqual(await myFavorites(), []); + }); + + test.each([ + { label: 'clipIdãŒnull', parameters: { clipId: null } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '2603966e-b865-426c-94a7-af4a01241dc1', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): User => bob, assertion: { + code: 'NOT_FAVORITED', + id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', + } }, + { label: 'ãŠæ°—ã«å…¥ã‚Šã—ã¦ã„ãªã„クリップ', assertion: { + code: 'NOT_FAVORITED', + id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', + } }, + ])('ã®è¨å®šè§£é™¤ã¯$labelãªã‚‰ã§ããªã„', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/unfavorite', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('ã‚’å–å¾—ã§ãる。', async () => { + await favorite({ clipId: aliceClip.id }); + const favorited = await myFavorites(); + assert.deepStrictEqual(favorited, [await show({ clipId: aliceClip.id })]); + }); + + test('ã‚’å–å¾—ã—ãŸã¨ã他人ã®ãŠæ°—ã«å…¥ã‚Šã¯å«ã¾ãªã„。', async () => { + await favorite({ clipId: aliceClip.id }); + const favorited = await myFavorites({ user: bob }); + assert.deepStrictEqual(favorited, []); + }); + }); + + describe('ã«ç´ã¥ãノート', () => { + let aliceClip: Clip; + + const sampleNotes = (): 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', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type RemoveNoteParam = JTDDataType<typeof RemoveNoteParamDef>; + const removeNote = async (parameters: RemoveNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => { + return successfulApiCall<void>({ + endpoint: '/clips/remove-note', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type NotesParam = JTDDataType<typeof NotesParamDef>; + const notes = async (parameters: Partial<NotesParam>, request: Partial<ApiRequest> = {}): Promise<Note[]> => { + return successfulApiCall<Note[]>({ + endpoint: '/clips/notes', + parameters, + user: alice, + ...request, + }); + }; + + beforeEach(async () => { + aliceClip = await create(); + }); + + test('ã‚’è¿½åŠ ã§ãる。', async () => { + await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + const res = await show({ clipId: aliceClip.id }); + assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString()); + assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), [aliceNote]); + + // 他人ã®éžå…¬é–‹ãƒŽãƒ¼ãƒˆã‚‚çªã£è¾¼ã‚ã‚‹ + await addNote({ clipId: aliceClip.id, noteId: bobHomeNote.id }); + await addNote({ clipId: aliceClip.id, noteId: bobFollowersNote.id }); + await addNote({ clipId: aliceClip.id, noteId: bobSpecifiedNote.id }); + }); + + test('ã¨ã—ã¦åŒã˜ãƒŽãƒ¼ãƒˆã‚’二回ç´ã¥ã‘ã‚‹ã“ã¨ã¯ã§ããªã„', async () => { + await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + await failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + }, + user: alice, + }, { + status: 400, + code: 'ALREADY_CLIPPED', + id: '734806c4-542c-463a-9311-15c512803965', + }); + }); + + // TODO: 17000msãらã„ã‹ã‹ã‚‹... + test('ã‚’ãƒãƒªã‚·ãƒ¼ã§å®šã‚られãŸä¸Šé™ã„ã£ã±ã„(200)ã‚’è¶…ãˆã¦è¿½åŠ ã¯ã§ããªã„。', async () => { + const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; + const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { + text: `test ${i}`, + }) as unknown)) as Note[]; + await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id }))); + + await failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + }, + user: alice, + }, { + status: 400, + code: 'TOO_MANY_CLIP_NOTES', + id: 'f0dba960-ff73-4615-8df4-d6ac5d9dc118', + }); + }); + + test('ã¯ä»–人ã®ã‚¯ãƒªãƒƒãƒ—ã¸è¿½åŠ ã§ããªã„。', async () => await failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + }, + user: bob, + }, { + status: 400, + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', + })); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', + } }, + { label: 'å˜åœ¨ã—ãªã„ノート', parameters: { noteId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_NOTE', + id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): object => bob, assetion: { + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', + } }, + ])('ã®è¿½åŠ ã¯$labelã ã¨ã§ããªã„', async ({ parameters, user, assetion }) => failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assetion, + })); + + test('を削除ã§ãる。', async () => { + await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + await removeNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), []); + }); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_CLIP', + id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteã¨ç•°ãªã‚‹ + } }, + { label: 'å˜åœ¨ã—ãªã„ノート', parameters: { noteId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_NOTE', + id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteã¨ç•°ãªã‚‹ + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): object => bob, assetion: { + code: 'NO_SUCH_CLIP', + id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteã¨ç•°ãªã‚‹ + } }, + ])('ã®å‰Šé™¤ã¯$labelã ã¨ã§ããªã„', async ({ parameters, user, assetion }) => failedApiCall({ + endpoint: '/clips/remove-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assetion, + })); + + test('ã‚’å–å¾—ã§ãる。', async () => { + const noteList = sampleNotes(); + for (const note of noteList) { + await addNote({ clipId: aliceClip.id, noteId: note.id }); + } + + const res = await notes({ clipId: aliceClip.id }); + + // 自分ã®ãƒŽãƒ¼ãƒˆã¯éžå…¬é–‹ã§ã‚‚入れられるã—ã€è¦‹ãˆã‚‹ + // 他人ã®éžå…¬é–‹ãƒŽãƒ¼ãƒˆã¯å…¥ã‚Œã‚‰ã‚Œã‚‹ã‘ã©ã€é™¤å¤–ã•れる + const expects = [ + aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, + bobNote, bobHomeNote, + ]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test('を始端IDã¨limitã§å–å¾—ã§ãる。', async () => { + const noteList = sampleNotes(); + noteList.sort(compareBy(s => s.id)); + for (const note of noteList) { + await addNote({ clipId: aliceClip.id, noteId: note.id }); + } + + const res = await notes({ + clipId: aliceClip.id, + sinceId: noteList[2].id, + limit: 3, + }); + + // Promise.allã§è¿”ã£ã¦ãã‚‹é…列ã¯IDé †ã§ä¸¦ã‚“ã§ãªã„ã®ã§ã‚½ãƒ¼ãƒˆã—ã¦åŽ³å¯†æ¯”è¼ƒ + const expects = [noteList[3], noteList[4], noteList[5]]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test('ã‚’ID範囲指定ã§å–å¾—ã§ãる。', async () => { + const noteList = sampleNotes(); + noteList.sort(compareBy(s => s.id)); + for (const note of noteList) { + await addNote({ clipId: aliceClip.id, noteId: note.id }); + } + + const res = await notes({ + clipId: aliceClip.id, + sinceId: noteList[1].id, + untilId: noteList[4].id, + }); + + // Promise.allã§è¿”ã£ã¦ãã‚‹é…列ã¯IDé †ã§ä¸¦ã‚“ã§ãªã„ã®ã§ã‚½ãƒ¼ãƒˆã—ã¦åŽ³å¯†æ¯”è¼ƒ + const expects = [noteList[2], noteList[3]]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test.todo('Remoteã®ãƒŽãƒ¼ãƒˆã‚‚クリップã§ãる。ã©ã†ãƒ†ã‚¹ãƒˆã—よã†ï¼Ÿ'); + + test('ã¯ä»–人ã®Publicãªã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰ã‚‚å–å¾—ã§ãる。', async () => { + const bobClip = await create({ isPublic: true }, { user: bob } ); + await addNote({ clipId: bobClip.id, noteId: aliceNote.id }, { user: bob }); + const res = await notes({ clipId: bobClip.id }); + assert.deepStrictEqual(res, [aliceNote]); + }); + + test('ã¯Publicãªã‚¯ãƒªãƒƒãƒ—ãªã‚‰èªè¨¼ãªã—ã§ã‚‚å–å¾—ã§ãる。(éžå…¬é–‹ãƒŽãƒ¼ãƒˆã¯hideã•れã¦è¿”ã£ã¦ãã‚‹)', async () => { + const publicClip = await create({ isPublic: true }); + await addNote({ clipId: publicClip.id, noteId: aliceNote.id }); + await addNote({ clipId: publicClip.id, noteId: aliceHomeNote.id }); + await addNote({ clipId: publicClip.id, noteId: aliceFollowersNote.id }); + await addNote({ clipId: publicClip.id, noteId: aliceSpecifiedNote.id }); + + const res = await notes({ clipId: publicClip.id }, { user: undefined }); + const expects = [ + aliceNote, aliceHomeNote, + // èªè¨¼ãªã—ã ã¨éžå…¬é–‹ãƒŽãƒ¼ãƒˆã¯çµæžœã«ã¯å«ã‚€ã‘ã©hideã•れる。 + hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote), + ]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test.todo('ブãƒãƒƒã‚¯ã€ãƒŸãƒ¥ãƒ¼ãƒˆã•れãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã®è¨å®šï¼†å–å¾—etc.'); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'limitゼãƒ', parameters: { limit: 0 } }, + { label: 'limit最大+1', parameters: { limit: 101 } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', + } }, + { label: '他人ã®Privateãªã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰', user: (): object => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', + } }, + { label: '未èªè¨¼ã§Privateãªã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰', user: (): undefined => undefined, assertion: { + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', + } }, + ])('ã¯$labelã ã¨å–å¾—ã§ããªã„', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/notes', + parameters: { + clipId: aliceClip.id, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + }); +}); diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index 42bdc5f24d..afb72c84d4 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -4,11 +4,11 @@ import * as assert from 'assert'; // node-fetch only supports it's own Blob yet // https://github.com/node-fetch/node-fetch/pull/1664 import { Blob } from 'node-fetch'; -import { startServer, signup, post, api, uploadFile } from '../utils.js'; +import { startServer, signup, post, api, uploadFile, simpleGet } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; describe('Endpoints', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; let alice: any; let bob: any; @@ -16,7 +16,7 @@ describe('Endpoints', () => { let dave: any; beforeAll(async () => { - p = await startServer(); + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); @@ -24,7 +24,7 @@ describe('Endpoints', () => { }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); describe('signup', () => { @@ -162,14 +162,14 @@ describe('Endpoints', () => { const res = await api('/users/show', { userId: '000000000000000000000000', }); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); }); test('é–“é•ã£ãŸIDã§æ€’られる', async () => { const res = await api('/users/show', { userId: 'kyoppie', }); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); }); }); @@ -206,7 +206,7 @@ describe('Endpoints', () => { describe('notes/reactions/create', () => { test('リアクションã§ãã‚‹', async () => { - const bobPost = await post(bob); + const bobPost = await post(bob, { text: 'hi' }); const res = await api('/notes/reactions/create', { noteId: bobPost.id, @@ -224,7 +224,7 @@ describe('Endpoints', () => { }); test('è‡ªåˆ†ã®æŠ•ç¨¿ã«ã‚‚リアクションã§ãã‚‹', async () => { - const myPost = await post(alice); + const myPost = await post(alice, { text: 'hi' }); const res = await api('/notes/reactions/create', { noteId: myPost.id, @@ -235,7 +235,7 @@ describe('Endpoints', () => { }); test('二é‡ã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã™ã‚‹ã¨ä¸Šæ›¸ãã•れる', async () => { - const bobPost = await post(bob); + const bobPost = await post(bob, { text: 'hi' }); await api('/notes/reactions/create', { noteId: bobPost.id, @@ -439,6 +439,45 @@ describe('Endpoints', () => { 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> => { + // drive/files/create does not expose webpublicType directly, so get it by posting it + const res = await post(user, { + text: mediaType, + fileIds: [fileId], + }); + const apRes = await simpleGet(`notes/${res.id}`, 'application/activity+json'); + assert.strictEqual(apRes.status, 200); + assert.ok(Array.isArray(apRes.body.attachment)); + return apRes.body.attachment[0].mediaType; + }; + + test(`逿˜Žãª${type}ファイルを作æˆã§ãã‚‹`, async () => { + const path = `with-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); + + const webpublicType = await getWebpublicType(alice, res.body.id); + assert.strictEqual(webpublicType, 'image/webp'); + }); + + test(`逿˜Žã˜ã‚ƒãªã„${type}ファイルを作æˆã§ãã‚‹`, async () => { + 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); + + const webpublicType = await getWebpublicType(alice, res.body.id); + assert.strictEqual(webpublicType, 'image/webp'); + }); + } }); describe('drive/files/update', () => { @@ -802,4 +841,12 @@ describe('Endpoints', () => { assert.strictEqual(res.body[0].id, carolPost.id); }); }); + + describe('URL preview', () => { + test('Error from summaly becomes HTTP 422', async () => { + const res = await simpleGet('/url?url=https://e:xample.com'); + assert.strictEqual(res.status, 422); + assert.strictEqual(res.body.error.code, 'URL_PREVIEW_FAILED'); + }); + }); }); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 6b3c795235..78ca8b43ba 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -1,7 +1,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { startServer, signup, post, api, simpleGet } from '../utils.js'; +import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js'; +import type { SimpleGetResponse } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; // Request Accept @@ -13,181 +14,448 @@ const UNSPECIFIED = '*/*'; // Response Content-Type const AP = 'application/activity+json; charset=utf-8'; const HTML = 'text/html; charset=utf-8'; +const JSON_UTF8 = 'application/json; charset=utf-8'; -describe('Fetch resource', () => { - let p: INestApplicationContext; +describe('Webリソース', () => { + let app: INestApplicationContext; let alice: any; + let aliceUploadedFile: any; let alicesPost: any; + let alicePage: any; + let alicePlay: any; + let aliceClip: any; + let aliceGalleryPost: any; + let aliceChannel: any; + + type Request = { + path: string, + accept?: string, + cookie?: string, + }; + const ok = async (param: Request & { + type?: string, + }):Promise<SimpleGetResponse> => { + const { path, accept, cookie, type } = param; + const res = await simpleGet(path, accept, cookie); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, type ?? HTML); + return res; + }; + + const notOk = async (param: Request & { + status?: number, + code?: string, + }): Promise<SimpleGetResponse> => { + const { path, accept, cookie, status, code } = param; + const res = await simpleGet(path, accept, cookie); + assert.notStrictEqual(res.status, 200); + if (status != null) { + assert.strictEqual(res.status, status); + } + if (code != null) { + assert.strictEqual(res.body.error.code, code); + } + return res; + }; + + const notFound = async (param: Request): Promise<SimpleGetResponse> => { + return await notOk({ + ...param, + status: 404, + }); + }; + + const metaTag = (res: SimpleGetResponse, key: string, superkey = 'name'): string => { + return res.body.window.document.querySelector('meta[' + superkey + '="' + key + '"]')?.content; + }; beforeAll(async () => { - p = await startServer(); + app = await startServer(); alice = await signup({ username: 'alice' }); + aliceUploadedFile = await uploadFile(alice); alicesPost = await post(alice, { text: 'test', }); + alicePage = await page(alice, {}); + alicePlay = await play(alice, {}); + aliceClip = await clip(alice, {}); + aliceGalleryPost = await galleryPost(alice, { + fileIds: [aliceUploadedFile.body.id], + }); + aliceChannel = await channel(alice, {}); }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); - describe('Common', () => { - test('meta', async () => { - const res = await api('/meta', { - }); - - assert.strictEqual(res.status, 200); - }); - - test('GET root', async () => { - const res = await simpleGet('/'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - }); + describe.each([ + { path: '/', type: HTML }, + { path: '/docs/ja-JP/about', type: HTML }, // "指定ã•れãŸURLã«è©²å½“ã™ã‚‹ãƒšãƒ¼ã‚¸ã¯ã‚りã¾ã›ã‚“ã§ã—ãŸã€‚" + // fastify-static gives charset=UTF-8 instead of utf-8 and that's okay + { path: '/api-doc', type: 'text/html; charset=UTF-8' }, + { path: '/api.json', type: JSON_UTF8 }, + { path: '/api-console', type: HTML }, + { path: '/_info_card_', type: HTML }, + { path: '/bios', type: HTML }, + { path: '/cli', type: HTML }, + { path: '/flush', type: HTML }, + { path: '/robots.txt', type: 'text/plain; charset=UTF-8' }, + { path: '/favicon.ico', type: 'image/vnd.microsoft.icon' }, + { path: '/opensearch.xml', type: 'application/opensearchdescription+xml' }, + { path: '/apple-touch-icon.png', type: 'image/png' }, + { path: '/twemoji/2764.svg', type: 'image/svg+xml' }, + { path: '/twemoji/2764-fe0f-200d-1f525.svg', type: 'image/svg+xml' }, + { path: '/twemoji-badge/2764.png', type: 'image/png' }, + { path: '/twemoji-badge/2764-fe0f-200d-1f525.png', type: 'image/png' }, + { path: '/fluent-emoji/2764.png', type: 'image/png' }, + { path: '/fluent-emoji/2764-fe0f-200d-1f525.png', type: 'image/png' }, + ])('$path', (p) => { + test('ãŒGETã§ãる。', async () => await ok({ ...p })); - test('GET docs', async () => { - const res = await simpleGet('/docs/ja-JP/about'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - }); + // 注æ„: WebページãŒ200ã§å–å¾—ã§ãã¦ã‚‚ã€å®Ÿéš›ã®HTMLãŒæ£ã—ã表示ã§ãã‚‹ã¨ã¯é™ã‚‰ãªã„ + // 例ãˆã°ã€ /@xxx/pages/yyy ã«å˜åœ¨ã—ãªã„IDを渡ã—ãŸå ´åˆã€HTTPレスãƒãƒ³ã‚¹ã§ã¯ã‚¨ãƒ©ãƒ¼ã‚’区別ã§ããªã„ + // ã“ã†ã„ã£ãŸã‚¢ã‚µãƒ¼ã‚·ãƒ§ãƒ³ã¯ãƒ•ãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰E2Eã‚„API Endpointã®ãƒ†ã‚¹ãƒˆã§æ‹…ä¿ã™ã‚‹ã€‚ + }); - test('GET api-doc (廃æ¢)', async () => { - const res = await simpleGet('/api-doc'); - assert.strictEqual(res.status, 404); - }); + describe.each([ + { path: '/twemoji/2764.png' }, + { path: '/twemoji/2764-fe0f-200d-1f525.png' }, + { path: '/twemoji-badge/2764.svg' }, + { path: '/twemoji-badge/2764-fe0f-200d-1f525.svg' }, + { path: '/fluent-emoji/2764.svg' }, + { path: '/fluent-emoji/2764-fe0f-200d-1f525.svg' }, + ])('$path', ({ path }) => { + test('ã¯GETã§ããªã„。', async () => await notFound({ path })); + }); - test('GET api.json (廃æ¢)', async () => { - const res = await simpleGet('/api.json'); - assert.strictEqual(res.status, 404); - }); + describe.each([ + { ext: 'rss', type: 'application/rss+xml; charset=utf-8' }, + { ext: 'atom', type: 'application/atom+xml; charset=utf-8' }, + { ext: 'json', type: 'application/json; charset=utf-8' }, + ])('/@:username.$ext', ({ ext, type }) => { + const path = (username: string): string => `/@${username}.${ext}`; - test('GET api/foo (å˜åœ¨ã—ãªã„)', async () => { - const res = await simpleGet('/api/foo'); - assert.strictEqual(res.status, 404); - assert.strictEqual(res.body.error.code, 'UNKNOWN_API_ENDPOINT'); - }); + test('ãŒGETã§ãる。', async () => await ok({ + path: path(alice.username), + type, + })); - test('GET favicon.ico', async () => { - const res = await simpleGet('/favicon.ico'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/vnd.microsoft.icon'); - }); + test('ã¯å˜åœ¨ã—ãªã„ユーザーã¯GETã§ããªã„。', async () => await notOk({ + path: path('nonexisting'), + status: 404, + })); + }); - test('GET apple-touch-icon.png', async () => { - const res = await simpleGet('/apple-touch-icon.png'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/png'); - }); + describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { + test('ã¯GETã§ããªã„。', async () => await notOk({ + path, + status: 404, + code: 'UNKNOWN_API_ENDPOINT', + })); + }); - test('GET twemoji svg', async () => { - const res = await simpleGet('/twemoji/2764.svg'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/svg+xml'); - }); + describe.each([{ path: '/queue' }])('$path', ({ path }) => { + test('ã¯adminã§ãªã‘れã°GETã§ããªã„。', async () => await notOk({ + path, + status: 500, // FIXME? 403ã§ã¯ãªã„。 + })); + + test('ã¯adminãªã‚‰GETã§ãる。', async () => await ok({ + path, + cookie: cookie(alice), + })); + }); - test('GET twemoji svg with hyphen', async () => { - const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/svg+xml'); - }); + describe.each([{ path: '/streaming' }])('$path', ({ path }) => { + test('ã¯GETã§ããªã„。', async () => await notOk({ + path, + status: 503, + })); }); describe('/@:username', () => { - test('Only AP => AP', async () => { - const res = await simpleGet(`/@${alice.username}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); + const path = (username: string): string => `/@${username}`; + + describe.each([ + { accept: PREFER_HTML }, + { accept: UNSPECIFIED }, + ])('(Acceptヘッダ: $accept)', ({ accept }) => { + test('ã¯HTMLã¨ã—ã¦GETã§ãる。', async () => { + const res = await ok({ + path: path(alice.username), + accept, + type: HTML, + }); + assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); + assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); + + // TODO ogã‚¿ã‚°ã®æ¤œè¨¼ + // TODO profile.noCrawleã®æ¤œè¨¼ + // TODO twitter:creatorã®æ¤œè¨¼ + // TODO <link rel="me" ...>ã®æ¤œè¨¼ + }); + test('ã¯HTMLã¨ã—ã¦GETã§ãる。(å˜åœ¨ã—ãªã„IDã§ã‚‚。)', async () => await ok({ + path: path('xxxxxxxxxx'), + type: HTML, + })); }); - test('Prefer AP => AP', async () => { - const res = await simpleGet(`/@${alice.username}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); + describe.each([ + { accept: ONLY_AP }, + { accept: PREFER_AP }, + ])('(Acceptヘッダ: $accept)', ({ accept }) => { + test('ã¯ActivityPubã¨ã—ã¦GETã§ãる。', async () => { + const res = await ok({ + path: path(alice.username), + accept, + type: AP, + }); + assert.strictEqual(res.body.type, 'Person'); + }); + + test('ã¯å˜åœ¨ã—ãªã„IDã®ã¨ãActivityPubã¨ã—ã¦GETã§ããªã„。', async () => await notFound({ + path: path('xxxxxxxxxx'), + accept, + })); }); + }); - test('Prefer HTML => HTML', async () => { - const res = await simpleGet(`/@${alice.username}`, PREFER_HTML); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); + describe.each([ + // 実際ã®ãƒãƒ³ãƒ‰ãƒ«ã¯ãƒ•ãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰(index.vue)ã§è¡Œã‚れる + { sub: 'home' }, + { sub: 'notes' }, + { sub: 'activity' }, + { sub: 'achievements' }, + { sub: 'reactions' }, + { sub: 'clips' }, + { sub: 'pages' }, + { sub: 'gallery' }, + ])('/@:username/$sub', ({ sub }) => { + const path = (username: string): string => `/@${username}/${sub}`; + + test('ã¯HTMLã¨ã—ã¦GETã§ãる。', async () => { + const res = await ok({ + path: path(alice.username), + }); + assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); + assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); }); + }); + + describe('/@:user/pages/:page', () => { + const path = (username: string, pagename: string): string => `/@${username}/pages/${pagename}`; - test('Unspecified => HTML', async () => { - const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); + test('ã¯HTMLã¨ã—ã¦GETã§ãる。', async () => { + const res = await ok({ + path: path(alice.username, alicePage.name), + }); + assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); + assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); + assert.strictEqual(metaTag(res, 'misskey:page-id'), alicePage.id); + + // TODO ogã‚¿ã‚°ã®æ¤œè¨¼ + // TODO profile.noCrawleã®æ¤œè¨¼ + // TODO twitter:creatorã®æ¤œè¨¼ }); + + test('ã¯GETã§ãる。(å˜åœ¨ã—ãªã„IDã§ã‚‚。)', async () => await ok({ + path: path(alice.username, 'xxxxxxxxxx'), + })); }); describe('/users/:id', () => { - test('Only AP => AP', async () => { - const res = await simpleGet(`/users/${alice.id}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - }); + const path = (id: string): string => `/users/${id}`; - test('Prefer AP => AP', async () => { - const res = await simpleGet(`/users/${alice.id}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - }); + describe.each([ + { accept: PREFER_HTML }, + { accept: UNSPECIFIED }, + ])('(Acceptヘッダ: $accept)', ({ accept }) => { + test('ã¯/@:usernameã«ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã™ã‚‹', async () => { + const res = await simpleGet(path(alice.id), accept); + assert.strictEqual(res.status, 302); + assert.strictEqual(res.location, `/@${alice.username}`); + }); - test('Prefer HTML => Redirect to /@:username', async () => { - const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML); - assert.strictEqual(res.status, 302); - assert.strictEqual(res.location, `/@${alice.username}`); + test('ã¯å˜åœ¨ã—ãªã„ユーザーã¯GETã§ããªã„。', async () => await notFound({ + path: path('xxxxxxxx'), + })); }); - test('Undecided => HTML', async () => { - const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED); - assert.strictEqual(res.status, 302); - assert.strictEqual(res.location, `/@${alice.username}`); + describe.each([ + { accept: ONLY_AP }, + { accept: PREFER_AP }, + ])('(Acceptヘッダ: $accept)', ({ accept }) => { + test('ã¯ActivityPubã¨ã—ã¦GETã§ãる。', async () => { + const res = await ok({ + path: path(alice.id), + accept, + type: AP, + }); + assert.strictEqual(res.body.type, 'Person'); + }); + + test('ã¯å˜åœ¨ã—ãªã„IDã®ã¨ãActivityPubã¨ã—ã¦GETã§ããªã„。', async () => await notOk({ + path: path('xxxxxxxx'), + accept, + status: 404, + })); }); }); + + describe('/users/inbox', () => { + test('ãŒGETã§ãる。(POST専用ã ã‘ã©4xx/5xxã«ãªã‚‰ãšHTMLãŒè¿”ã£ã¦ãã‚‹)', async () => await ok({ + path: '/inbox', + })); - describe('/notes/:id', () => { - test('Only AP => AP', async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); + // test.todo('POSTã§ãる?'); + }); + + describe('/users/:id/inbox', () => { + const path = (id: string): string => `/users/${id}/inbox`; + + test('ãŒGETã§ãる。(POST専用ã ã‘ã©4xx/5xxã«ãªã‚‰ãšHTMLãŒè¿”ã£ã¦ãã‚‹)', async () => await ok({ + path: path(alice.id), + })); + + // test.todo('POSTã§ãる?'); + }); + + describe('/users/:id/outbox', () => { + const path = (id: string): string => `/users/${id}/outbox`; + + test('ãŒGETã§ãる。', async () => { + const res = await ok({ + path: path(alice.id), + type: AP, + }); + assert.strictEqual(res.body.type, 'OrderedCollection'); }); + }); + + describe('/notes/:id', () => { + const path = (noteId: string): string => `/notes/${noteId}`; - test('Prefer AP => AP', async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); + describe.each([ + { accept: PREFER_HTML }, + { accept: UNSPECIFIED }, + ])('(Acceptヘッダ: $accept)', ({ accept }) => { + test('ã¯HTMLã¨ã—ã¦GETã§ãる。', async () => { + const res = await ok({ + path: path(alicesPost.id), + accept, + type: HTML, + }); + assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); + assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); + assert.strictEqual(metaTag(res, 'misskey:note-id'), alicesPost.id); + + // TODO ogã‚¿ã‚°ã®æ¤œè¨¼ + // TODO profile.noCrawleã®æ¤œè¨¼ + // TODO twitter:creatorã®æ¤œè¨¼ + }); + + test('ã¯HTMLã¨ã—ã¦GETã§ãる。(å˜åœ¨ã—ãªã„IDã§ã‚‚。)', async () => await ok({ + path: path('xxxxxxxxxx'), + })); }); - test('Prefer HTML => HTML', async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); + describe.each([ + { accept: ONLY_AP }, + { accept: PREFER_AP }, + ])('(Acceptヘッダ: $accept)', ({ accept }) => { + test('ã¯ActivityPubã¨ã—ã¦GETã§ãる。', async () => { + const res = await ok({ + path: path(alicesPost.id), + accept, + type: AP, + }); + assert.strictEqual(res.body.type, 'Note'); + }); + + test('ã¯å˜åœ¨ã—ãªã„IDã®ã¨ãActivityPubã¨ã—ã¦GETã§ããªã„。', async () => await notFound({ + path: path('xxxxxxxxxx'), + accept, + })); }); + }); + + describe('/play/:id', () => { + const path = (playid: string): string => `/play/${playid}`; - test('Unspecified => HTML', async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); + test('ãŒGETã§ãる。', async () => { + const res = await ok({ + path: path(alicePlay.id), + }); + assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); + assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); + assert.strictEqual(metaTag(res, 'misskey:flash-id'), alicePlay.id); + + // TODO ogã‚¿ã‚°ã®æ¤œè¨¼ + // TODO profile.noCrawleã®æ¤œè¨¼ + // TODO twitter:creatorã®æ¤œè¨¼ }); + + test('ãŒGETã§ãる。(å˜åœ¨ã—ãªã„IDã§ã‚‚。)', async () => await ok({ + path: path('xxxxxxxxxx'), + })); }); + + describe('/clips/:clip', () => { + const path = (clip: string): string => `/clips/${clip}`; + + test('ãŒGETã§ãる。', async () => { + const res = await ok({ + path: path(aliceClip.id), + }); + assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); + assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); + assert.strictEqual(metaTag(res, 'misskey:clip-id'), aliceClip.id); - describe('Feeds', () => { - test('RSS', async () => { - const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8'); + // TODO ogã‚¿ã‚°ã®æ¤œè¨¼ + // TODO profile.noCrawleã®æ¤œè¨¼ }); + + test('ãŒGETã§ãる。(å˜åœ¨ã—ãªã„IDã§ã‚‚。)', async () => await ok({ + path: path('xxxxxxxxxx'), + })); + }); + + describe('/gallery/:post', () => { + const path = (post: string): string => `/gallery/${post}`; + + test('ãŒGETã§ãる。', async () => { + const res = await ok({ + path: path(aliceGalleryPost.id), + }); + assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); + assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); - test('ATOM', async () => { - const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8'); + // FIXME: misskey:gallery-post-idã¿ãŸã„ãªmetaã‚¿ã‚°ã®è¨å®šãŒãªã„ + // TODO profile.noCrawleã®æ¤œè¨¼ + // TODO twitter:creatorã®æ¤œè¨¼ }); + + test('ãŒGETã§ãる。(å˜åœ¨ã—ãªã„IDã§ã‚‚。)', async () => await ok({ + path: path('xxxxxxxxxx'), + })); + }); + + describe('/channels/:channel', () => { + const path = (channel: string): string => `/channels/${channel}`; + + test('ã¯GETã§ãる。', async () => { + const res = await ok({ + path: path(aliceChannel.id), + }); - test('JSON', async () => { - const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'application/json; charset=utf-8'); + // FIXME: misskey関連ã®metaã‚¿ã‚°ã®è¨å®šãŒãªã„ + // TODO ogã‚¿ã‚°ã®æ¤œè¨¼ }); + + test('ãŒGETã§ãる。(å˜åœ¨ã—ãªã„IDã§ã‚‚。)', async () => await ok({ + path: path('xxxxxxxxxx'), + })); }); }); diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index d53919ca1e..7b75005a39 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -5,19 +5,19 @@ import { signup, api, startServer, simpleGet } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; describe('FF visibility', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; let alice: any; let bob: any; beforeAll(async () => { - p = await startServer(); + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); test('ffVisibility ㌠public ãªãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ•ã‚©ãƒãƒ¼/フォãƒãƒ¯ãƒ¼ã‚’誰ã§ã‚‚見れる', async () => { diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index 6654a290be..25bd532cfb 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -5,7 +5,7 @@ import { signup, api, post, react, startServer, waitFire } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; describe('Mute', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; // alice mutes carol let alice: any; @@ -13,14 +13,14 @@ describe('Mute', () => { let carol: any; beforeAll(async () => { - p = await startServer(); + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); test('ミュート作æˆ', async () => { @@ -76,9 +76,9 @@ describe('Mute', () => { describe('Timeline', () => { test('タイムラインã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { - const aliceNote = await post(alice); - const bobNote = await post(bob); - const carolNote = await post(carol); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi' }); + const carolNote = await post(carol, { text: 'hi' }); const res = await api('/notes/local-timeline', {}, alice); @@ -90,8 +90,8 @@ describe('Mute', () => { }); test('タイムラインã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ã®RenoteãŒå«ã¾ã‚Œãªã„', async () => { - const aliceNote = await post(alice); - const carolNote = await post(carol); + const aliceNote = await post(alice, { text: 'hi' }); + const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { renoteId: carolNote.id, }); @@ -108,7 +108,7 @@ describe('Mute', () => { describe('Notification', () => { test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®é€šçŸ¥ãŒå«ã¾ã‚Œãªã„(リアクション)', async () => { - const aliceNote = await post(alice); + const aliceNote = await post(alice, { text: 'hi' }); await react(bob, aliceNote, 'like'); await react(carol, aliceNote, 'like'); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 1b5f9580d5..e87045a8cf 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -6,14 +6,14 @@ import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } fro import type { INestApplicationContext } from '@nestjs/common'; describe('Note', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; let Notes: any; let alice: any; let bob: any; beforeAll(async () => { - p = await startServer(); + app = await startServer(); const connection = await initTestDb(true); Notes = connection.getRepository(Note); alice = await signup({ username: 'alice' }); @@ -21,7 +21,7 @@ describe('Note', () => { }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); test('投稿ã§ãã‚‹', async () => { @@ -136,6 +136,31 @@ describe('Note', () => { assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); }); + test('visibility: followersã§renoteã§ãã‚‹', async () => { + const createRes = await api('/notes/create', { + text: 'test', + visibility: 'followers', + }, alice); + + assert.strictEqual(createRes.status, 200); + + const renoteId = createRes.body.createdNote.id; + const renoteRes = await api('/notes/create', { + visibility: 'followers', + renoteId, + }, alice); + + assert.strictEqual(renoteRes.status, 200); + assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId); + assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers'); + + const deleteRes = await api('/notes/delete', { + noteId: renoteRes.body.createdNote.id, + }, alice); + + assert.strictEqual(deleteRes.status, 204); + }); + test('æ–‡å—æ•°ãŽã‚ŠãŽã‚Šã§æ€’られãªã„', async () => { const post = { text: '!'.repeat(3000), diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts new file mode 100644 index 0000000000..0f73b8d09f --- /dev/null +++ b/packages/backend/test/e2e/renote-mute.ts @@ -0,0 +1,85 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('Renote Mute', () => { + let app: INestApplicationContext; + + // alice mutes carol + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + app = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + test('ミュート作æˆ', async () => { + const res = await api('/renote-mute/create', { + userId: carol.id, + }, alice); + + assert.strictEqual(res.status, 204); + }); + + test('タイムラインã«ãƒªãƒŽãƒ¼ãƒˆãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { + const bobNote = await post(bob, { text: 'hi' }); + const carolRenote = await post(carol, { renoteId: bobNote.id }); + const carolNote = await post(carol, { text: 'hi' }); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + }); + + test('タイムラインã«ãƒªãƒŽãƒ¼ãƒˆãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®å¼•用ãŒå«ã¾ã‚Œã‚‹', async () => { + const bobNote = await post(bob, { text: 'hi' }); + const carolRenote = await post(carol, { renoteId: bobNote.id, text: 'kore' }); + const carolNote = await post(carol, { text: 'hi' }); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + }); + + test('ストリームã«ãƒªãƒŽãƒ¼ãƒˆãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®ãƒªãƒŽãƒ¼ãƒˆãŒæµã‚Œãªã„', async () => { + const bobNote = await post(bob, { text: 'hi' }); + + const fired = await waitFire( + alice, 'localTimeline', + () => api('notes/create', { renoteId: bobNote.id }, carol), + msg => msg.type === 'note' && msg.body.userId === carol.id, + ); + + assert.strictEqual(fired, false); + }); + + test('ストリームã«ãƒªãƒŽãƒ¼ãƒˆãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®å¼•ç”¨ãŒæµã‚Œã‚‹', async () => { + const bobNote = await post(bob, { text: 'hi' }); + + const fired = await waitFire( + alice, 'localTimeline', + () => api('notes/create', { renoteId: bobNote.id, text: 'kore' }, carol), + msg => msg.type === 'note' && msg.body.userId === carol.id, + ); + + assert.strictEqual(fired, true); + }); +}); diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 23c431f2e7..e1b690c30f 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -6,7 +6,7 @@ import { connectStream, signup, api, post, startServer, initTestDb, waitFire } f import type { INestApplicationContext } from '@nestjs/common'; describe('Streaming', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; let Followings: any; const follow = async (follower: any, followee: any) => { @@ -38,7 +38,7 @@ describe('Streaming', () => { let list: any; beforeAll(async () => { - p = await startServer(); + app = await startServer(); const connection = await initTestDb(true); Followings = connection.getRepository(Following); @@ -74,7 +74,7 @@ describe('Streaming', () => { }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); describe('Events', () => { diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 792436d88f..2ae2eb67c1 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -5,21 +5,21 @@ import { signup, api, post, connectStream, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; describe('Note thread mute', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; let alice: any; let bob: any; let carol: any; beforeAll(async () => { - p = await startServer(); + app = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); }, 1000 * 60 * 2); afterAll(async () => { - await p.close(); + await app.close(); }); test('notes/mentions ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„ã‚‹ã‚¹ãƒ¬ãƒƒãƒ‰ã®æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index 690cba1746..c11099e7b5 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -5,7 +5,7 @@ import { signup, api, post, uploadUrl, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; describe('users/notes', () => { - let p: INestApplicationContext; + let app: INestApplicationContext; let alice: any; let jpgNote: any; @@ -13,7 +13,7 @@ describe('users/notes', () => { let jpgPngNote: any; beforeAll(async () => { - p = await startServer(); + app = await startServer(); alice = await signup({ username: 'alice' }); const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); @@ -29,7 +29,7 @@ describe('users/notes', () => { }, 1000 * 60 * 2); afterAll(async() => { - await p.close(); + await app.close(); }); test('ファイルタイプ指定 (jpg)', async () => { diff --git a/packages/backend/test/resources/with-alpha.avif b/packages/backend/test/resources/with-alpha.avif Binary files differnew file mode 100644 index 0000000000..05f494212e --- /dev/null +++ b/packages/backend/test/resources/with-alpha.avif diff --git a/packages/backend/test/resources/with-alpha.webp b/packages/backend/test/resources/with-alpha.webp Binary files differnew file mode 100644 index 0000000000..d7b0d70b7f --- /dev/null +++ b/packages/backend/test/resources/with-alpha.webp diff --git a/packages/backend/test/resources/without-alpha.avif b/packages/backend/test/resources/without-alpha.avif Binary files differnew file mode 100644 index 0000000000..9ea23608b8 --- /dev/null +++ b/packages/backend/test/resources/without-alpha.avif diff --git a/packages/backend/test/resources/without-alpha.webp b/packages/backend/test/resources/without-alpha.webp Binary files differnew file mode 100644 index 0000000000..a51091efe2 --- /dev/null +++ b/packages/backend/test/resources/without-alpha.webp diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts new file mode 100644 index 0000000000..0549800a68 --- /dev/null +++ b/packages/backend/test/unit/DriveService.ts @@ -0,0 +1,55 @@ +process.env.NODE_ENV = 'test'; + +import { jest } from '@jest/globals'; +import { Test } from '@nestjs/testing'; +import { GlobalModule } from '@/GlobalModule.js'; +import { DriveService } from '@/core/DriveService.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import { S3Service } from '@/core/S3Service'; +import type { Meta } from '@/models'; +import type { DeleteObjectOutput } from 'aws-sdk/clients/s3'; +import type { AWSError } from 'aws-sdk/lib/error'; +import type { PromiseResult, Request } from 'aws-sdk/lib/request'; +import type { TestingModule } from '@nestjs/testing'; + +describe('DriveService', () => { + let app: TestingModule; + let driveService: DriveService; + + beforeEach(async () => { + app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + providers: [DriveService, S3Service], + }).compile(); + app.enableShutdownHooks(); + driveService = app.get<DriveService>(DriveService); + + const s3Service = app.get<S3Service>(S3Service); + const s3 = s3Service.getS3({} as Meta); + + // new S3() surprisingly does not return an instance of class S3. + // Let's use getPrototypeOf here to get a real prototype, since spying on S3.prototype doesn't work. + // TODO: Use `aws-sdk-client-mock` package when upgrading to AWS SDK v3. + jest.spyOn(Object.getPrototypeOf(s3), 'deleteObject').mockImplementation(() => { + // Roughly mock AWS request object + return { + async promise(): Promise<PromiseResult<DeleteObjectOutput, AWSError>> { + const err = new Error('mock') as AWSError; + err.code = 'NoSuchKey'; + throw err; + }, + } as Request<DeleteObjectOutput, AWSError>; + }); + }); + + describe('Object storage', () => { + test('delete a file with no valid key', async () => { + try { + await driveService.deleteObjectStorageFile('lol no way'); + } catch (err: any) { + console.log(err.cause); + throw err; + } + }); + }); +}); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 3d0032507e..146998937e 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -7,15 +7,35 @@ import { jest } from '@jest/globals'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; +import type { IActor } from '@/core/activitypub/type.js'; import { MockResolver } from '../misc/mock-resolver.js'; +import { Note } from '@/models/index.js'; + +const host = 'https://host1.test'; + +function createRandomActor(): IActor & { id: string } { + const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; + const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + id: actorId, + type: 'Person', + preferredUsername, + inbox: `${actorId}/inbox`, + outbox: `${actorId}/outbox`, + }; +} describe('ActivityPub', () => { let noteService: ApNoteService; let personService: ApPersonService; + let rendererService: ApRendererService; let resolver: MockResolver; beforeEach(async () => { @@ -28,6 +48,7 @@ describe('ActivityPub', () => { noteService = app.get<ApNoteService>(ApNoteService); personService = app.get<ApPersonService>(ApPersonService); + rendererService = app.get<ApRendererService>(ApRendererService); resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService)); // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error @@ -36,18 +57,7 @@ describe('ActivityPub', () => { }); describe('Parse minimum object', () => { - const host = 'https://host1.test'; - const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; - const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; - - const actor = { - '@context': 'https://www.w3.org/ns/activitystreams', - id: actorId, - type: 'Person', - preferredUsername, - inbox: `${actorId}/inbox`, - outbox: `${actorId}/outbox`, - }; + const actor = createRandomActor(); const post = { '@context': 'https://www.w3.org/ns/activitystreams', @@ -80,29 +90,40 @@ describe('ActivityPub', () => { }); }); - describe('Truncate long name', () => { - const host = 'https://host1.test'; - const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; - const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; + describe('Name field', () => { + test('Truncate long name', async () => { + const actor = { + ...createRandomActor(), + name: rndstr('0-9a-z', 129), + }; - const name = rndstr('0-9a-z', 129); + resolver._register(actor.id, actor); - const actor = { - '@context': 'https://www.w3.org/ns/activitystreams', - id: actorId, - type: 'Person', - preferredUsername, - name, - inbox: `${actorId}/inbox`, - outbox: `${actorId}/outbox`, - }; + const user = await personService.createPerson(actor.id, resolver); + + assert.deepStrictEqual(user.name, actor.name.slice(0, 128)); + }); + + test('Normalize empty name', async () => { + const actor = { + ...createRandomActor(), + name: '', + }; - test('Actor', async () => { resolver._register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); - assert.deepStrictEqual(user.name, actor.name.substr(0, 128)); + assert.strictEqual(user.name, null); + }); + }); + + describe('Renderer', () => { + test('Render an announce with visibility: followers', () => { + rendererService.renderAnnounce(null, { + createdAt: new Date(0), + visibility: 'followers', + } as Note); }); }); }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 8203e49359..4f501a8726 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,8 +1,11 @@ +import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { isAbsolute, basename } from 'node:path'; +import { inspect } from 'node:util'; import WebSocket from 'ws'; import fetch, { Blob, File, RequestInit } from 'node-fetch'; import { DataSource } from 'typeorm'; +import { JSDOM } from 'jsdom'; import { entities } from '../src/postgres.js'; import { loadConfig } from '../src/config.js'; import type * as misskey from 'misskey-js'; @@ -12,11 +15,45 @@ export { server as startServer } from '@/boot/common.js'; const config = loadConfig(); export const port = config.port; +export const cookie = (me: any): string => { + return `token=${me.token};`; +}; + export const api = async (endpoint: string, params: any, me?: any) => { const normalized = endpoint.replace(/^\//, ''); return await request(`api/${normalized}`, params, me); }; +export type ApiRequest = { + endpoint: string, + parameters: object, + user: object | undefined, +}; + +export const successfulApiCall = async <T, >(request: ApiRequest, assertion: { + status: number, +} = { status: 200 }): Promise<T> => { + const { endpoint, parameters, user } = request; + const { status } = assertion; + const res = await api(endpoint, parameters, user); + assert.strictEqual(res.status, status, inspect(res.body)); + return res.body; +}; + +export const failedApiCall = async <T, >(request: ApiRequest, assertion: { + status: number, + code: string, + id: string +}): Promise<T> => { + const { endpoint, parameters, user } = request; + const { status, code, id } = assertion; + const res = await api(endpoint, parameters, user); + assert.strictEqual(res.status, status, inspect(res.body)); + assert.strictEqual(res.body.error.code, code, inspect(res.body)); + assert.strictEqual(res.body.error.id, id, inspect(res.body)); + return res.body; +}; + const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { const auth = me ? { i: me.token, @@ -57,15 +94,28 @@ export const signup = async (params?: any): Promise<any> => { }; export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => { - const q = Object.assign({ - text: 'test', - }, params); + const q = params; const res = await api('notes/create', q, user); return res.body ? res.body.createdNote : null; }; +// éžå…¬é–‹ãƒŽãƒ¼ãƒˆã‚’APIè¶Šã—ã«è¦‹ãŸã¨ãã®ãƒŽãƒ¼ãƒˆ NoteEntityService.ts +export const hiddenNote = (note: any): any => { + const temp = { + ...note, + fileIds: [], + files: [], + text: null, + cw: null, + isHidden: true, + }; + delete temp.visibleUserIds; + delete temp.poll; + return temp; +}; + export const react = async (user: any, note: any, reaction: string): Promise<any> => { await api('notes/reactions/create', { noteId: note.id, @@ -73,6 +123,71 @@ export const react = async (user: any, note: any, reaction: string): Promise<any }, user); }; +export const page = async (user: any, page: any = {}): Promise<any> => { + const res = await api('pages/create', { + alignCenter: false, + content: [ + { + id: '2be9a64b-5ada-43a3-85f3-ec3429551ded', + text: 'Hello World!', + type: 'text', + }, + ], + eyeCatchingImageId: null, + font: 'sans-serif', + hideTitleWhenPinned: false, + name: '1678594845072', + script: '', + summary: null, + title: '', + variables: [], + ...page, + }, user); + return res.body; +}; + +export const play = async (user: any, play: any = {}): Promise<any> => { + const res = await api('flash/create', { + permissions: [], + script: 'test', + summary: '', + title: 'test', + ...play, + }, user); + return res.body; +}; + +export const clip = async (user: any, clip: any = {}): Promise<any> => { + const res = await api('clips/create', { + description: null, + isPublic: true, + name: 'test', + ...clip, + }, user); + return res.body; +}; + +export const galleryPost = async (user: any, channel: any = {}): Promise<any> => { + const res = await api('gallery/posts/create', { + description: null, + fileIds: [], + isSensitive: false, + title: 'test', + ...channel, + }, user); + return res.body; +}; + +export const channel = async (user: any, channel: any = {}): Promise<any> => { + const res = await api('channels/create', { + bannerId: null, + description: null, + name: 'test', + ...channel, + }, user); + return res.body; +}; + interface UploadOptions { /** Optional, absolute path or relative from ./resources/ */ path?: string | URL; @@ -198,17 +313,33 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond }); }; -export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status: number, body: any, type: string | null, location: string | null }> => { +export type SimpleGetResponse = { + status: number, + body: any | JSDOM | null, + type: string | null, + location: string | null +}; +export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise<SimpleGetResponse> => { const res = await relativeFetch(path, { headers: { Accept: accept, + Cookie: cookie, }, redirect: 'manual', }); - const body = res.headers.get('content-type') === 'application/json; charset=utf-8' - ? await res.json() - : null; + const jsonTypes = [ + 'application/json; charset=utf-8', + 'application/activity+json; charset=utf-8', + ]; + const htmlTypes = [ + 'text/html; charset=utf-8', + ]; + + const body = + jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : + htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : + null; return { status: res.status, diff --git a/packages/frontend/package.json b/packages/frontend/package.json index e4c04f5937..54404c8c53 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,6 +4,8 @@ "scripts": { "watch": "vite", "build": "vite build", + "test": "vitest --run", + "test-and-coverage": "vitest --run --coverage", "typecheck": "vue-tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", "lint": "pnpm typecheck && pnpm eslint" @@ -13,8 +15,8 @@ "@rollup/plugin-alias": "4.0.3", "@rollup/plugin-json": "6.0.0", "@rollup/pluginutils": "5.0.2", - "@syuilo/aiscript": "0.12.4", - "@tabler/icons-webfont": "2.2.0", + "@syuilo/aiscript": "0.13.1", + "@tabler/icons-webfont": "2.10.0", "@vitejs/plugin-vue": "4.0.0", "@vue/compiler-sfc": "3.2.47", "autobind-decorator": "2.4.0", @@ -46,7 +48,7 @@ "punycode": "2.3.0", "querystring": "0.2.1", "rndstr": "1.0.0", - "rollup": "3.17.3", + "rollup": "3.19.0", "s-age": "1.1.2", "sanitize-html": "2.10.0", "sass": "1.58.3", @@ -54,10 +56,10 @@ "strict-event-emitter-types": "2.0.0", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", - "three": "0.150.0", + "three": "0.150.1", "throttle-debounce": "5.0.0", "tinycolor2": "1.6.0", - "tsc-alias": "1.8.2", + "tsc-alias": "1.8.3", "tsconfig-paths": "4.1.2", "twemoji-parser": "14.0.0", "typescript": "4.9.5", @@ -70,28 +72,34 @@ "vuedraggable": "next" }, "devDependencies": { + "@testing-library/vue": "^6.6.1", "@types/escape-regexp": "0.0.1", "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", "@types/matter-js": "0.18.2", - "@types/node": "18.14.1", + "@types/node": "18.15.0", "@types/punycode": "2.1.0", - "@types/sanitize-html": "2.8.0", + "@types/sanitize-html": "2.8.1", "@types/seedrandom": "3.0.5", "@types/throttle-debounce": "5.0.0", "@types/tinycolor2": "1.4.3", "@types/uuid": "9.0.1", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.53.0", - "@typescript-eslint/parser": "5.53.0", + "@typescript-eslint/eslint-plugin": "5.54.1", + "@typescript-eslint/parser": "5.54.1", + "@vitest/coverage-c8": "^0.29.2", "@vue/runtime-core": "3.2.47", "cross-env": "7.0.3", "cypress": "12.7.0", "eslint": "8.35.0", "eslint-plugin-import": "2.27.5", "eslint-plugin-vue": "9.9.0", - "start-server-and-test": "1.15.4", + "happy-dom": "8.9.0", + "start-server-and-test": "2.0.0", + "summaly": "github:misskey-dev/summaly", + "vitest": "^0.29.2", + "vitest-fetch-mock": "^0.2.2", "vue-eslint-parser": "9.1.0", "vue-tsc": "1.2.0" } diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 610212b6ec..9b104391d7 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -1,4 +1,4 @@ -import { defineAsyncComponent, reactive } from 'vue'; +import { defineAsyncComponent, reactive, ref } from 'vue'; import * as misskey from 'misskey-js'; import { showSuspendedDialog } from './scripts/show-suspended-dialog'; import { i18n } from './i18n'; @@ -7,6 +7,7 @@ import { del, get, set } from '@/scripts/idb-proxy'; import { apiUrl } from '@/config'; import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; +import { MenuButton } from './types/menu'; // TODO: ä»–ã®ã‚¿ãƒ–ã¨æ°¸ç¶šåŒ–ã•れãŸstateã‚’åŒæœŸ @@ -26,11 +27,11 @@ export function incNotesCount() { } export async function signout() { + if (!$i) return; + waiting(); miLocalStorage.removeItem('account'); - await removeAccount($i.id); - const accounts = await getAccounts(); //#region Remove service worker registration @@ -76,15 +77,19 @@ export async function addAccount(id: Account['id'], token: Account['token']) { } } -export async function removeAccount(id: Account['id']) { +export async function removeAccount(idOrToken: Account['id']) { const accounts = await getAccounts(); - accounts.splice(accounts.findIndex(x => x.id === id), 1); + const i = accounts.findIndex(x => x.id === idOrToken || x.token === idOrToken); + if (i !== -1) accounts.splice(i, 1); - if (accounts.length > 0) await set('accounts', accounts); - else await del('accounts'); + if (accounts.length > 0) { + await set('accounts', accounts); + } else { + await del('accounts'); + } } -function fetchAccount(token: string): Promise<Account> { +function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> { return new Promise((done, fail) => { // Fetch user window.fetch(`${apiUrl}/i`, { @@ -96,44 +101,94 @@ function fetchAccount(token: string): Promise<Account> { 'Content-Type': 'application/json', }, }) - .then(res => res.json()) - .then(res => { - if (res.error) { - if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { - showSuspendedDialog().then(() => { - signout(); + .then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => { + if (res.status >= 500 && res.status < 600) { + // サーãƒãƒ¼ã‚¨ãƒ©ãƒ¼(5xx)ã®å ´åˆã‚’rejectã¨ã™ã‚‹ + // (èªè¨¼ã‚¨ãƒ©ãƒ¼ãªã©4xxã¯resolve) + return fail2(res); + } + res.json().then(done2, fail2); + })) + .then(async res => { + if (res.error) { + if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { + // SUSPENDED + if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { + await showSuspendedDialog(); + } + } else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') { + // USER_IS_DELETED + // アカウントãŒå‰Šé™¤ã•れã¦ã„ã‚‹ + if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { + await alert({ + type: 'error', + title: i18n.ts.accountDeleted, + text: i18n.ts.accountDeletedDescription, }); - } else { - alert({ + } + } else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') { + // AUTHENTICATION_FAILED + // トークンãŒç„¡åŠ¹åŒ–ã•れã¦ã„ãŸã‚Šã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒå‰Šé™¤ã•れãŸã‚Šã—ã¦ã„ã‚‹ + if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { + await alert({ type: 'error', - title: i18n.ts.failedToFetchAccountInformation, - text: JSON.stringify(res.error), + title: i18n.ts.tokenRevoked, + text: i18n.ts.tokenRevokedDescription, }); } } else { - res.token = token; - done(res); + await alert({ + type: 'error', + title: i18n.ts.failedToFetchAccountInformation, + text: JSON.stringify(res.error), + }); } - }) - .catch(fail); + + // rejectã‹ã¤ç†ç”±ãŒtrueã®å ´åˆã€å‰Šé™¤å¯¾è±¡ã§ã‚ã‚‹ã“ã¨ã‚’示㙠+ fail(true); + } else { + (res as Account).token = token; + done(res as Account); + } + }) + .catch(fail); }); } -export function updateAccount(accountData) { +export function updateAccount(accountData: Partial<Account>) { + if (!$i) return; for (const [key, value] of Object.entries(accountData)) { $i[key] = value; } miLocalStorage.setItem('account', JSON.stringify($i)); } -export function refreshAccount() { - return fetchAccount($i.token).then(updateAccount); +export async function refreshAccount() { + if (!$i) return; + return fetchAccount($i.token, $i.id) + .then(updateAccount, reason => { + if (reason === true) return signout(); + return; + }); } export async function login(token: Account['token'], redirect?: string) { - waiting(); + const showing = ref(true); + popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { + success: false, + showing: showing, + }, {}, 'closed'); if (_DEV_) console.log('logging as token ', token); - const me = await fetchAccount(token); + const me = await fetchAccount(token, undefined, true) + .catch(reason => { + if (reason === true) { + // 削除対象ã®å ´åˆ + removeAccount(token); + } + + showing.value = false; + throw reason; + }); miLocalStorage.setItem('account', JSON.stringify(me)); document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardã®èªè¨¼ã¨ã‹ã§ä½¿ã† await addAccount(me.id, token); @@ -155,6 +210,8 @@ export async function openAccountMenu(opts: { active?: misskey.entities.UserDetailed['id']; onChoose?: (account: misskey.entities.UserDetailed) => void; }, ev: MouseEvent) { + if (!$i) return; + function showSigninDialog() { popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { done: res => { @@ -175,8 +232,9 @@ export async function openAccountMenu(opts: { async function switchAccount(account: misskey.entities.UserDetailed) { const storedAccounts = await getAccounts(); - const token = storedAccounts.find(x => x.id === account.id).token; - switchAccountWithToken(token); + const found = storedAccounts.find(x => x.id === account.id); + if (found == null) return; + switchAccountWithToken(found.token); } function switchAccountWithToken(token: string) { @@ -188,7 +246,7 @@ export async function openAccountMenu(opts: { function createItem(account: misskey.entities.UserDetailed) { return { - type: 'user', + type: 'user' as const, user: account, active: opts.active != null ? opts.active === account.id : false, action: () => { @@ -201,22 +259,29 @@ export async function openAccountMenu(opts: { }; } - const accountItemPromises = storedAccounts.map(a => new Promise(res => { + const accountItemPromises = storedAccounts.map(a => new Promise<ReturnType<typeof createItem> | MenuButton>(res => { accountsPromise.then(accounts => { const account = accounts.find(x => x.id === a.id); - if (account == null) return res(null); + if (account == null) return res({ + type: 'button' as const, + text: a.id, + action: () => { + switchAccountWithToken(a.token); + }, + }); + res(createItem(account)); }); })); if (opts.withExtraOperation) { popupMenu([...[{ - type: 'link', + type: 'link' as const, text: i18n.ts.profile, to: `/@${ $i.username }`, avatar: $i, }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { - type: 'parent', + type: 'parent' as const, icon: 'ti ti-plus', text: i18n.ts.addAccount, children: [{ @@ -227,7 +292,7 @@ export async function openAccountMenu(opts: { action: () => { createAccount(); }, }], }, { - type: 'link', + type: 'link' as const, icon: 'ti ti-users', text: i18n.ts.manageAccounts, to: '/settings/accounts', diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index c72cc2ab1b..1875b507ca 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -10,7 +10,8 @@ import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch } from 'vu import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; -type Captcha = { +// APIs provided by Captcha services +export type Captcha = { render(container: string | Node, options: { readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown; }): string; @@ -32,7 +33,7 @@ declare global { const props = defineProps<{ provider: CaptchaProvider; - sitekey: string; + sitekey: string | null; // null will show error on request modelValue?: string | null; }>(); diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue new file mode 100644 index 0000000000..c5fb718782 --- /dev/null +++ b/packages/frontend/src/components/MkClipPreview.vue @@ -0,0 +1,39 @@ +<template> +<div :class="$style.root" class="_panel"> + <b>{{ clip.name }}</b> + <div v-if="clip.description" :class="$style.description">{{ clip.description }}</div> + <div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div> + <div :class="$style.user"> + <MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/> + </div> +</div> +</template> + +<script lang="ts" setup> +import { i18n } from '@/i18n'; + +defineProps<{ + clip: any; +}>(); +</script> + +<style lang="scss" module> +.root { + display: block; + padding: 16px; +} + +.description { + padding: 8px 0; +} + +.user { + padding-top: 16px; + border-top: solid 0.5px var(--divider); +} + +.userAvatar { + width: 32px; + height: 32px; +} +</style> diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 4525d3a009..d6303f9675 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -1,7 +1,9 @@ <script lang="ts"> import { defineComponent, h, PropType, TransitionGroup, useCssModule } from 'vue'; import MkAd from '@/components/global/MkAd.vue'; +import { isDebuggerEnabled, stackTraceInstances } from '@/debug'; import { i18n } from '@/i18n'; +import * as os from '@/os'; import { defaultStore } from '@/store'; import { MisskeyEntity } from '@/types/date-separated-list'; @@ -46,7 +48,7 @@ export default defineComponent({ if (props.items.length === 0) return; - const renderChildren = () => props.items.map((item, i) => { + const renderChildrenImpl = () => props.items.map((item, i) => { if (!slots || !slots.default) return; const el = slots.default({ @@ -95,6 +97,21 @@ export default defineComponent({ } }); + const renderChildren = () => { + const children = renderChildrenImpl(); + if (isDebuggerEnabled(6864)) { + const nodes = children.flatMap((node) => node ?? []); + const keys = new Set(nodes.map((node) => node.key)); + if (keys.size !== nodes.length) { + const id = crypto.randomUUID(); + const instances = stackTraceInstances(); + os.toast(instances.reduce((a, c) => `${a} at ${c.type.name}`, `[DEBUG_6864 (${id})]: ${nodes.length - keys.size} duplicated keys found`)); + console.warn({ id, debugId: 6864, stack: instances }); + } + } + return children; + }; + function onBeforeLeave(el: HTMLElement) { el.style.top = `${el.offsetTop}px`; el.style.left = `${el.offsetLeft}px`; diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 8c17c0530a..ab408b5008 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -32,14 +32,14 @@ </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, ref } from 'vue'; +import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import bytes from '@/filters/bytes'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { $i } from '@/account'; +import { getDriveFileMenu } from '@/scripts/get-drive-file-menu'; const props = withDefaults(defineProps<{ file: Misskey.entities.DriveFile; @@ -60,48 +60,16 @@ const isDragging = ref(false); const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`); -function getMenu() { - return [{ - text: i18n.ts.rename, - icon: 'ti ti-forms', - action: rename, - }, { - text: props.file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: props.file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-off', - action: toggleSensitive, - }, { - text: i18n.ts.describeFile, - icon: 'ti ti-text-caption', - action: describe, - }, null, { - text: i18n.ts.copyUrl, - icon: 'ti ti-link', - action: copyUrl, - }, { - type: 'a', - href: props.file.url, - target: '_blank', - text: i18n.ts.download, - icon: 'ti ti-download', - download: props.file.name, - }, null, { - text: i18n.ts.delete, - icon: 'ti ti-trash', - danger: true, - action: deleteFile, - }]; -} - function onClick(ev: MouseEvent) { if (props.selectMode) { emit('chosen', props.file); } else { - os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + os.popupMenu(getDriveFileMenu(props.file), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); } } function onContextmenu(ev: MouseEvent) { - os.contextMenu(getMenu(), ev); + os.contextMenu(getDriveFileMenu(props.file), ev); } function onDragstart(ev: DragEvent) { @@ -118,62 +86,6 @@ function onDragend() { isDragging.value = false; emit('dragend'); } - -function rename() { - os.inputText({ - title: i18n.ts.renameFile, - placeholder: i18n.ts.inputNewFileName, - default: props.file.name, - }).then(({ canceled, result: name }) => { - if (canceled) return; - os.api('drive/files/update', { - fileId: props.file.id, - name: name, - }); - }); -} - -function describe() { - os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { - default: props.file.comment != null ? props.file.comment : '', - file: props.file, - }, { - done: caption => { - os.api('drive/files/update', { - fileId: props.file.id, - comment: caption.length === 0 ? null : caption, - }); - }, - }, 'closed'); -} - -function toggleSensitive() { - os.api('drive/files/update', { - fileId: props.file.id, - isSensitive: !props.file.isSensitive, - }); -} - -function copyUrl() { - copyToClipboard(props.file.url); - os.success(); -} -/* -function addApp() { - alert('not implemented yet'); -} -*/ -async function deleteFile() { - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }), - }); - - if (canceled) return; - os.api('drive/files/delete', { - fileId: props.file.id, - }); -} </script> <style lang="scss" scoped> diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index c418ac2c52..89abf1d946 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -8,7 +8,9 @@ <button v-for="emoji in emojis" :key="emoji" + :data-emoji="emoji" class="_button item" + @pointerenter="computeButtonTitle" @click="emit('chosen', emoji, $event)" > <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> @@ -20,6 +22,7 @@ <script lang="ts" setup> import { ref, computed, Ref } from 'vue'; +import { getEmojiName } from '@/scripts/emojilist'; const props = defineProps<{ emojis: string[] | Ref<string[]>; @@ -33,4 +36,12 @@ const emit = defineEmits<{ const emojis = computed(() => Array.isArray(props.emojis) ? props.emojis : props.emojis.value); const shown = ref(!!props.initialShown); + +/** @see MkEmojiPicker.vue */ +function computeButtonTitle(ev: MouseEvent): void { + const elm = ev.target as HTMLElement; + const emoji = elm.dataset.emoji as string; + elm.title = getEmojiName(emoji) ?? emoji; +} + </script> diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 7d280f2f4b..a5a39108d6 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -35,8 +35,10 @@ <button v-for="emoji in pinned" :key="emoji" + :data-emoji="emoji" class="_button item" tabindex="0" + @pointerenter="computeButtonTitle" @click="chosen(emoji, $event)" > <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> @@ -52,6 +54,8 @@ v-for="emoji in recentlyUsedEmojis" :key="emoji" class="_button item" + :data-emoji="emoji" + @pointerenter="computeButtonTitle" @click="chosen(emoji, $event)" > <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> @@ -90,7 +94,7 @@ import { ref, shallowRef, computed, watch, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import XSection from '@/components/MkEmojiPicker.section.vue'; -import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist'; +import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName } from '@/scripts/emojilist'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; @@ -291,6 +295,13 @@ function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`; } +/** @see MkEmojiPicker.section.vue */ +function computeButtonTitle(ev: MouseEvent): void { + const elm = ev.target as HTMLElement; + const emoji = elm.dataset.emoji as string; + elm.title = getEmojiName(emoji) ?? emoji; +} + function chosen(emoji: any, ev?: MouseEvent) { const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; if (el) { diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index b777a1329b..a4065dcd07 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -3,21 +3,24 @@ <ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/> <div :class="$style.hiddenText"> <div :class="$style.hiddenTextWrapper"> - <b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ $ts.sensitive }}</b> - <span style="display: block;">{{ $ts.clickToShow }}</span> + <b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}</b> + <span style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </div> </div> -<div v-else :class="$style.visible" :style="defaultStore.state.darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'"> +<div v-else :class="$style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'"> <a :class="$style.imageContainer" :href="image.url" :title="image.name" > <ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :cover="false"/> - <div v-if="image.type === 'image/gif'" :class="$style.gif">GIF</div> </a> - <button v-tooltip="$ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button> + <div :class="$style.indicators"> + <div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div> + <div v-if="image.comment" :class="$style.indicator">ALT</div> + </div> + <button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button> </div> </template> @@ -27,6 +30,7 @@ import * as misskey from 'misskey-js'; import { getStaticImageUrl } from '@/scripts/media-proxy'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import { defaultStore } from '@/store'; +import { i18n } from '@/i18n'; const props = defineProps<{ image: misskey.entities.DriveFile; @@ -34,11 +38,12 @@ const props = defineProps<{ }>(); let hide = $ref(true); +let darkMode = $ref(defaultStore.state.darkMode); const url = (props.raw || defaultStore.state.loadRawImages) ? props.image.url : defaultStore.state.disableShowingAnimatedImages - ? getStaticImageUrl(props.image.thumbnailUrl) + ? getStaticImageUrl(props.image.url) : props.image.thumbnailUrl; // Plugin:register_note_view_interruptor を使ã£ã¦æ›¸ãæ›ãˆã‚‰ã‚Œã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ãŸã‚watchã™ã‚‹ @@ -108,18 +113,25 @@ watch(() => props.image, () => { background-repeat: no-repeat; } -.gif { - background-color: var(--fg); +.indicators { + display: inline-flex; + position: absolute; + top: 12px; + left: 12px; + text-align: center; + pointer-events: none; + opacity: .5; + font-size: 14px; + gap: 6px; +} + +.indicator { + /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */ + background-color: black; border-radius: 6px; color: var(--accentLighten); display: inline-block; - font-size: 14px; font-weight: bold; - left: 12px; - opacity: .5; padding: 0 6px; - text-align: center; - top: 12px; - pointer-events: none; } </style> diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index c768a086cd..d36cc2d26b 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -118,7 +118,7 @@ onMounted(() => { }); lightbox.init(); - + window.addEventListener('popstate', () => { if (lightbox.pswp && lightbox.pswp.isOpen === true) { lightbox.pswp.close(); @@ -239,5 +239,6 @@ const previewable = (file: misskey.entities.DriveFile): boolean => { max-height: 8em; overflow-y: auto; text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px; + white-space: pre-line; } </style> diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index 4529d61c2f..852c72f6ff 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -8,7 +8,7 @@ :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened" > <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> - <div class="_modalBg data-cy-bg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent, 'data-cy-transparent': isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div> + <div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div> <div ref="content" :class="[$style.content, { [$style.fixed]: fixed }]" :style="{ zIndex }" @click.self="onBgClick"> <slot :max-height="maxHeight" :type="type"></slot> </div> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index bb1269562d..af81051a54 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -103,7 +103,8 @@ <i class="ti ti-ban"></i> </button> <button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()"> - <i class="ti ti-plus"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> + <i v-else class="ti ti-plus"></i> </button> <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)"> <i class="ti ti-minus"></i> @@ -329,18 +330,32 @@ function reply(viaKeyboard = false): void { function react(viaKeyboard = false): void { pleaseLogin(); - blur(); - reactionPicker.show(reactButton.value, reaction => { + if (appearNote.reactionAcceptance === 'likeOnly') { os.api('notes/reactions/create', { noteId: appearNote.id, - reaction: reaction, + reaction: 'â¤ï¸', }); - if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { - claimAchievement('reactWithoutRead'); + const el = reactButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - }, () => { - focus(); - }); + } else { + blur(); + reactionPicker.show(reactButton.value, reaction => { + os.api('notes/reactions/create', { + noteId: appearNote.id, + reaction: reaction, + }); + if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } + }, () => { + focus(); + }); + } } function undoReact(note): void { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index f5f4a2afc1..ea72e1b517 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -108,7 +108,8 @@ <i class="ti ti-ban"></i> </button> <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()"> - <i class="ti ti-plus"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> + <i v-else class="ti ti-plus"></i> </button> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <i class="ti ti-minus"></i> @@ -323,18 +324,32 @@ function reply(viaKeyboard = false): void { function react(viaKeyboard = false): void { pleaseLogin(); - blur(); - reactionPicker.show(reactButton.value, reaction => { + if (appearNote.reactionAcceptance === 'likeOnly') { os.api('notes/reactions/create', { noteId: appearNote.id, - reaction: reaction, + reaction: 'â¤ï¸', }); - if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { - claimAchievement('reactWithoutRead'); + const el = reactButton.value as HTMLElement | null | undefined; + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - }, () => { - focus(); - }); + } else { + blur(); + reactionPicker.show(reactButton.value, reaction => { + os.api('notes/reactions/create', { + noteId: appearNote.id, + reaction: reaction, + }); + if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { + claimAchievement('reactWithoutRead'); + } + }, () => { + focus(); + }); + } } function undoReact(note): void { diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index b38a4afa8b..2b541e6094 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -47,6 +47,9 @@ const showContent = $ref(false); width: 34px; height: 34px; border-radius: 8px; + position: sticky !important; + top: calc(16px + var(--stickyTop, 0px)); + left: 0; } .main { diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 38bf416ea8..b60967de02 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -69,8 +69,9 @@ <span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> <template v-else-if="notification.type === 'receiveFollowRequest'"> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span> - <div v-if="full && !followRequestDone"> - <button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button> + <div v-if="full && !followRequestDone" :class="$style.followRequestCommands"> + <MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton> + <MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton> </div> </template> <span v-else-if="notification.type === 'app'" :class="$style.text"> @@ -87,6 +88,7 @@ import * as misskey from 'misskey-js'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import XReactionTooltip from '@/components/MkReactionTooltip.vue'; +import MkButton from '@/components/MkButton.vue'; import { getNoteSummary } from '@/scripts/get-note-summary'; import { notePage } from '@/filters/note'; import { userPage } from '@/filters/user'; @@ -294,6 +296,16 @@ useTooltip(reactionRef, (showing) => { margin-left: 4px; } +.followRequestCommands { + display: flex; + gap: 8px; + max-width: 300px; + margin-top: 8px; +} +.followRequestCommandButton { + flex: 1; +} + @container (max-width: 600px) { .root { padding: 16px; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 09f672be7b..b1800f3af7 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -53,14 +53,23 @@ <XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/> <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <XNotePreview v-if="showPreview" :class="$style.preview" :text="text"/> + <div v-if="showingOptions" style="padding: 0 16px;"> + <MkSelect v-model="reactionAcceptance" small> + <template #label>{{ i18n.ts.reactionAcceptance }}</template> + <option :value="null">{{ i18n.ts.all }}</option> + <option value="likeOnly">{{ i18n.ts.likeOnly }}</option> + <option value="likeOnlyForRemote">{{ i18n.ts.likeOnlyForRemote }}</option> + </MkSelect> + </div> + <button v-tooltip="i18n.ts.emoji" class="_button" :class="$style.emojiButton" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> <footer :class="$style.footer"> <button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button> <button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button> <button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button> <button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button> <button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button> - <button v-tooltip="i18n.ts.emoji" class="_button" :class="$style.footerButton" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button> + <button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button> </footer> <datalist id="hashtags"> <option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/> @@ -76,6 +85,7 @@ import * as misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; import { toASCII } from 'punycode/'; import * as Acct from 'misskey-js/built/acct'; +import MkSelect from './MkSelect.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import XNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; @@ -151,12 +161,14 @@ let visibleUsers = $ref([]); if (props.initialVisibleUsers) { props.initialVisibleUsers.forEach(pushVisibleUser); } +let reactionAcceptance = $ref(defaultStore.state.reactionAcceptance); let autocomplete = $ref(null); let draghover = $ref(false); let quoteId = $ref(null); let hasNotSpecifiedMentions = $ref(false); let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]')); let imeText = $ref(''); +let showingOptions = $ref(false); const draftKey = $computed((): string => { let key = props.channel ? `channel:${props.channel.id}` : ''; @@ -166,7 +178,7 @@ const draftKey = $computed((): string => { } else if (props.reply) { key += `reply:${props.reply.id}`; } else { - key += 'note'; + key += `note:${$i.id}`; } return key; @@ -422,6 +434,10 @@ function pushVisibleUser(user) { function addVisibleUser() { os.selectUser().then(user => { pushVisibleUser(user); + + if (!text.toLowerCase().includes(`@${user.username.toLowerCase()}`)) { + text = `@${Acct.toString(user)} ${text}`; + } }); } @@ -610,6 +626,7 @@ async function post(ev?: MouseEvent) { localOnly: localOnly, visibility: visibility, visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined, + reactionAcceptance, }; if (withHashtags && hashtags && hashtags.trim() !== '') { @@ -1026,6 +1043,18 @@ defineExpose({ } } +.emojiButton { + position: absolute; + top: 55px; + right: 13px; + display: inline-block; + padding: 0; + margin: 0; + font-size: 1em; + width: 32px; + height: 32px; +} + @container (max-width: 500px) { .header { height: 50px; diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue index 8326ec7ef3..85c009f746 100644 --- a/packages/frontend/src/components/MkRetentionHeatmap.vue +++ b/packages/frontend/src/components/MkRetentionHeatmap.vue @@ -36,9 +36,11 @@ async function renderChart() { const wide = rootEl.offsetWidth > 600; const narrow = rootEl.offsetWidth < 400; - const maxDays = wide ? 15 : narrow ? 5 : 10; + const maxDays = wide ? 10 : narrow ? 5 : 7; - const raw = await os.api('retention', { }); + let raw = await os.api('retention', { }); + + raw = raw.slice(0, maxDays); const data = []; for (const record of raw) { @@ -60,10 +62,9 @@ async function renderChart() { const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; // 視覚上ã®åˆ†ã‹ã‚Šã‚„ã™ã•ã®ãŸã‚上ã‹ã‚‰æœ€ã‚‚大ãã„3ã¤ã®å€¤ã®å¹³å‡ã‚’最大値ã¨ã™ã‚‹ - //const max = raw.readWrite.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3; - const max = 4; + const max = raw.map(x => x.users).slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3; - const marginEachCell = 6; + const marginEachCell = 12; chartInstance = new Chart(chartEl, { type: 'matrix', diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 83506b8f66..08e41d6ae5 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -20,7 +20,7 @@ import MkSignin from '@/components/MkSignin.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n'; -const props = withDefaults(defineProps<{ +withDefaults(defineProps<{ autoSet?: boolean; message?: string, }>(), { @@ -29,7 +29,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'done'): void; + (ev: 'done', v: any): void; (ev: 'closed'): void; (ev: 'cancelled'): void; }>(); @@ -38,11 +38,11 @@ const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>(); function onClose() { emit('cancelled'); - dialog.close(); + if (dialog) dialog.close(); } function onLogin(res) { emit('done', res); - dialog.close(); + if (dialog) dialog.close(); } </script> diff --git a/packages/frontend/src/components/MkSignup.vue b/packages/frontend/src/components/MkSignup.vue index 62ada6b736..30279148f8 100644 --- a/packages/frontend/src/components/MkSignup.vue +++ b/packages/frontend/src/components/MkSignup.vue @@ -72,7 +72,7 @@ import { toUnicode } from 'punycode/'; import MkButton from './MkButton.vue'; import MkInput from './MkInput.vue'; import MkSwitch from './MkSwitch.vue'; -import MkCaptcha from '@/components/MkCaptcha.vue'; +import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; import * as config from '@/config'; import * as os from '@/os'; import { login } from '@/account'; @@ -92,9 +92,9 @@ const emit = defineEmits<{ const host = toUnicode(config.host); -let hcaptcha = $ref(); -let recaptcha = $ref(); -let turnstile = $ref(); +let hcaptcha = $ref<Captcha | undefined>(); +let recaptcha = $ref<Captcha | undefined>(); +let turnstile = $ref<Captcha | undefined>(); let username: string = $ref(''); let password: string = $ref(''); @@ -110,6 +110,8 @@ let ToSAgreement: boolean = $ref(false); let hCaptchaResponse = $ref(null); let reCaptchaResponse = $ref(null); let turnstileResponse = $ref(null); +let usernameAbortController: null | AbortController = $ref(null); +let emailAbortController: null | AbortController = $ref(null); const shouldDisableSubmitting = $computed((): boolean => { return submitting || @@ -117,7 +119,9 @@ const shouldDisableSubmitting = $computed((): boolean => { instance.enableHcaptcha && !hCaptchaResponse || instance.enableRecaptcha && !reCaptchaResponse || instance.enableTurnstile && !turnstileResponse || - passwordRetypeState === 'not-match'; + instance.emailRequiredForSignup && emailState !== 'ok' || + usernameState !== 'ok' || + passwordRetypeState !== 'match'; }); function onChangeUsername(): void { @@ -139,14 +143,20 @@ function onChangeUsername(): void { } } + if (usernameAbortController != null) { + usernameAbortController.abort(); + } usernameState = 'wait'; + usernameAbortController = new AbortController(); os.api('username/available', { username, - }).then(result => { + }, undefined, usernameAbortController.signal).then(result => { usernameState = result.available ? 'ok' : 'unavailable'; - }).catch(() => { - usernameState = 'error'; + }).catch((err) => { + if (err.name !== 'AbortError') { + usernameState = 'error'; + } }); } @@ -156,11 +166,15 @@ function onChangeEmail(): void { return; } + if (emailAbortController != null) { + emailAbortController.abort(); + } emailState = 'wait'; + emailAbortController = new AbortController(); os.api('email-address/available', { emailAddress: email, - }).then(result => { + }, undefined, emailAbortController.signal).then(result => { emailState = result.available ? 'ok' : result.reason === 'used' ? 'unavailable:used' : result.reason === 'format' ? 'unavailable:format' : @@ -168,8 +182,10 @@ function onChangeEmail(): void { result.reason === 'mx' ? 'unavailable:mx' : result.reason === 'smtp' ? 'unavailable:smtp' : 'unavailable'; - }).catch(() => { - emailState = 'error'; + }).catch((err) => { + if (err.name !== 'AbortError') { + emailState = 'error'; + } }); } @@ -192,19 +208,20 @@ function onChangePasswordRetype(): void { passwordRetypeState = password === retypedPassword ? 'match' : 'not-match'; } -function onSubmit(): void { +async function onSubmit(): Promise<void> { if (submitting) return; submitting = true; - os.api('signup', { - username, - password, - emailAddress: email, - invitationCode, - 'hcaptcha-response': hCaptchaResponse, - 'g-recaptcha-response': reCaptchaResponse, - 'turnstile-response': turnstileResponse, - }).then(() => { + try { + await os.api('signup', { + username, + password, + emailAddress: email, + invitationCode, + 'hcaptcha-response': hCaptchaResponse, + 'g-recaptcha-response': reCaptchaResponse, + 'turnstile-response': turnstileResponse, + }); if (instance.emailRequiredForSignup) { os.alert({ type: 'success', @@ -213,28 +230,27 @@ function onSubmit(): void { }); emit('signupEmailPending'); } else { - os.api('signin', { + const res = await os.api('signin', { username, password, - }).then(res => { - emit('signup', res); - - if (props.autoSet) { - login(res.i); - } }); + emit('signup', res); + + if (props.autoSet) { + return login(res.i); + } } - }).catch(() => { + } catch { submitting = false; - hcaptcha.reset?.(); - recaptcha.reset?.(); - turnstile.reset?.(); + hcaptcha?.reset?.(); + recaptcha?.reset?.(); + turnstile?.reset?.(); os.alert({ type: 'error', text: i18n.ts.somethingHappened, }); - }); + } } </script> diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 5381ecbfa5..094709e093 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -1,7 +1,18 @@ <template> -<template v-if="playerEnabled"> - <div :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`"> - <iframe v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/> +<template v-if="player.url && playerEnabled"> + <div + :class="$style.player" + :style="player.width ? `padding: ${(player.height || 0) / player.width * 100}% 0 0` : `padding: ${(player.height || 0)}px 0 0`" + > + <iframe + v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" + sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin" + scrolling="no" + :allow="player.allow.join(';')" + :class="$style.playerIframe" + :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" + :style="{ border: 0 }" + ></iframe> <span v-else>invalid url</span> </div> <div :class="$style.action"> @@ -28,7 +39,7 @@ <header :class="$style.header"> <h1 v-if="unknownUrl" :class="$style.title">{{ url }}</h1> <h1 v-else-if="fetching" :class="$style.title"><MkEllipsis/></h1> - <h1 v-else :class="$style.title" :title="title">{{ title }}</h1> + <h1 v-else :class="$style.title" :title="title ?? undefined">{{ title }}</h1> </header> <p v-if="unknownUrl" :class="$style.text">{{ i18n.ts.cannotLoad }}</p> <p v-else-if="fetching" :class="$style.text"><MkEllipsis/></p> @@ -37,7 +48,7 @@ <img v-if="icon" :class="$style.siteIcon" :src="icon"/> <p v-if="unknownUrl" :class="$style.siteName">?</p> <p v-else-if="fetching" :class="$style.siteName"><MkEllipsis/></p> - <p v-else :class="$style.siteName" :title="sitename">{{ sitename }}</p> + <p v-else :class="$style.siteName" :title="sitename ?? undefined">{{ sitename }}</p> </footer> </article> </component> @@ -59,6 +70,7 @@ <script lang="ts" setup> import { defineAsyncComponent, onUnmounted } from 'vue'; +import type { summaly } from 'summaly'; import { url as local } from '@/config'; import { i18n } from '@/i18n'; import * as os from '@/os'; @@ -66,6 +78,8 @@ import { deviceKind } from '@/scripts/device-kind'; import MkButton from '@/components/MkButton.vue'; import { versatileLang } from '@/scripts/intl-const'; +type SummalyResult = Awaited<ReturnType<typeof summaly>>; + const props = withDefaults(defineProps<{ url: string; detail?: boolean; @@ -91,7 +105,7 @@ let player = $ref({ url: null, width: null, height: null, -}); +} as SummalyResult['player']); let playerEnabled = $ref(false); let tweetId = $ref<string | null>(null); let tweetExpanded = $ref(props.detail); @@ -114,11 +128,7 @@ if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/ requestUrl.hash = ''; window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`).then(res => { - res.json().then(info => { - if (info.url == null) { - unknownUrl = true; - return; - } + res.json().then((info: SummalyResult) => { title = info.title; description = info.description; thumbnail = info.thumbnail; diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index 19c735c5f8..d074fdd150 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -19,9 +19,9 @@ @update:model-value="v => emit('updateWidgets', v)" > <template #item="{element}"> - <div :class="[$style.widget, $style['customize-container']]" class="data-cy-customize-container"> + <div :class="[$style.widget, $style['customize-container']]" data-cy-customize-container> <button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> - <button :class="$style['customize-container-remove']" class="_button data-cy-customize-container-remove" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> + <button :class="$style['customize-container-remove']" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> <div class="handle"> <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @update-props="updateWidget(element.id, $event)"/> </div> diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 2bb432e15f..e0304c8bc5 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -82,7 +82,7 @@ const choseAd = (): Ad | null => { }; const chosen = ref(choseAd()); -const shouldHide = $ref($i && $i.policies.canHideAds); +const shouldHide = $ref($i && $i.policies.canHideAds && (props.specify == null)); function reduceFrequency(): void { if (chosen.value == null) return; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index d392ec6d6f..7fb830d537 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -1,5 +1,5 @@ <template> -<span v-if="!link" v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: $store.state.squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> +<span v-if="!link" v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> <img :class="$style.inner" :src="url" decoding="async"/> <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> <template v-if="user.isCat"> @@ -7,7 +7,7 @@ <div :class="$style.earRight"/> </template> </span> -<MkA v-else v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: $store.state.squareAvatars }]" :style="{ color }" :title="acct(user)" :to="userPage(user)" :target="target"> +<MkA v-else v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" :to="userPage(user)" :target="target"> <img :class="$style.inner" :src="url" decoding="async"/> <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> <template v-if="user.isCat"> @@ -26,6 +26,8 @@ import { acct, userPage } from '@/filters/user'; import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; import { defaultStore } from '@/store'; +const squareAvatars = $ref(defaultStore.state.squareAvatars); + const props = withDefaults(defineProps<{ user: misskey.entities.User; target?: string | null; diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 560870f84c..63e8fc225c 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -20,26 +20,32 @@ import MkSpacer from './global/MkSpacer.vue'; import MkStickyContainer from './global/MkStickyContainer.vue'; export default function(app: App) { - app.component('I18n', I18n); - app.component('RouterView', RouterView); - app.component('Mfm', Mfm); - app.component('MkA', MkA); - app.component('MkAcct', MkAcct); - app.component('MkAvatar', MkAvatar); - app.component('MkEmoji', MkEmoji); - app.component('MkCustomEmoji', MkCustomEmoji); - app.component('MkUserName', MkUserName); - app.component('MkEllipsis', MkEllipsis); - app.component('MkTime', MkTime); - app.component('MkUrl', MkUrl); - app.component('MkLoading', MkLoading); - app.component('MkError', MkError); - app.component('MkAd', MkAd); - app.component('MkPageHeader', MkPageHeader); - app.component('MkSpacer', MkSpacer); - app.component('MkStickyContainer', MkStickyContainer); + for (const [key, value] of Object.entries(components)) { + app.component(key, value); + } } +export const components = { + I18n: I18n, + RouterView: RouterView, + Mfm: Mfm, + MkA: MkA, + MkAcct: MkAcct, + MkAvatar: MkAvatar, + MkEmoji: MkEmoji, + MkCustomEmoji: MkCustomEmoji, + MkUserName: MkUserName, + MkEllipsis: MkEllipsis, + MkTime: MkTime, + MkUrl: MkUrl, + MkLoading: MkLoading, + MkError: MkError, + MkAd: MkAd, + MkPageHeader: MkPageHeader, + MkSpacer: MkSpacer, + MkStickyContainer: MkStickyContainer, +}; + declare module '@vue/runtime-core' { export interface GlobalComponents { I18n: typeof I18n; diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 46ebc7d6a3..1d1b8fcea4 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -53,6 +53,7 @@ export const ROLE_POLICIES = [ 'canPublicNote', 'canInvite', 'canManageCustomEmojis', + 'canSearchNotes', 'canHideAds', 'driveCapacityMb', 'pinLimit', diff --git a/packages/frontend/src/debug.ts b/packages/frontend/src/debug.ts new file mode 100644 index 0000000000..5715acf674 --- /dev/null +++ b/packages/frontend/src/debug.ts @@ -0,0 +1,27 @@ +import { type ComponentInternalInstance, getCurrentInstance } from 'vue'; + +export function isDebuggerEnabled(id: number): boolean { + try { + return localStorage.getItem(`DEBUG_${id}`) !== null; + } catch { + return false; + } +} + +export function switchDebuggerEnabled(id: number, enabled: boolean): void { + if (enabled) { + localStorage.setItem(`DEBUG_${id}`, ''); + } else { + localStorage.removeItem(`DEBUG_${id}`); + } +} + +export function stackTraceInstances(): ComponentInternalInstance[] { + let instance = getCurrentInstance(); + const stack: ComponentInternalInstance[] = []; + while (instance) { + stack.push(instance); + instance = instance.parent; + } + return stack; +} diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts index 854f0a544e..064ee4f64b 100644 --- a/packages/frontend/src/directives/index.ts +++ b/packages/frontend/src/directives/index.ts @@ -14,17 +14,23 @@ import adaptiveBg from './adaptive-bg'; import container from './container'; export default function(app: App) { - app.directive('userPreview', userPreview); - app.directive('user-preview', userPreview); - app.directive('get-size', getSize); - app.directive('ripple', ripple); - app.directive('tooltip', tooltip); - app.directive('hotkey', hotkey); - app.directive('appear', appear); - app.directive('anim', anim); - app.directive('click-anime', clickAnime); - app.directive('panel', panel); - app.directive('adaptive-border', adaptiveBorder); - app.directive('adaptive-bg', adaptiveBg); - app.directive('container', container); + for (const [key, value] of Object.entries(directives)) { + app.directive(key, value); + } } + +export const directives = { + 'userPreview': userPreview, + 'user-preview': userPreview, + 'get-size': getSize, + 'ripple': ripple, + 'tooltip': tooltip, + 'hotkey': hotkey, + 'appear': appear, + 'anim': anim, + 'click-anime': clickAnime, + 'panel': panel, + 'adaptive-border': adaptiveBorder, + 'adaptive-bg': adaptiveBg, + 'container': container, +}; diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts index 0a626b36c6..a2dff87e8e 100644 --- a/packages/frontend/src/init.ts +++ b/packages/frontend/src/init.ts @@ -343,7 +343,9 @@ stream.on('_disconnected_', async () => { }); for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { - import('./plugin').then(({ install }) => { + import('./plugin').then(async ({ install }) => { + // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 + await new Promise(r => setTimeout(r, 0)); install(plugin); }); } diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index efc0e8c920..0e2f787d50 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -2,6 +2,7 @@ import { computed, reactive } from 'vue'; import { $i } from './account'; import { miLocalStorage } from './local-storage'; import { openInstanceMenu } from './ui/_common_/common'; +import { lookup } from './scripts/lookup'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { ui } from '@/config'; @@ -44,6 +45,13 @@ export const navbarItemDef = reactive({ icon: 'ti ti-search', to: '/search', }, + lookup: { + title: i18n.ts.lookup, + icon: 'ti ti-world-search', + action: (ev) => { + lookup(); + }, + }, lists: { title: i18n.ts.lists, icon: 'ti ti-list', @@ -136,4 +144,10 @@ export const navbarItemDef = reactive({ location.reload(); }, }, + profile: { + title: i18n.ts.profile, + icon: 'ti ti-user', + show: computed(() => $i != null), + to: `/@${$i?.username}`, + }, }); diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 3c073fc7c4..60f61ed293 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -19,7 +19,7 @@ <div style="text-align: center;"> {{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a> </div> - <div style="text-align: center;"> + <div v-if="$i != null" style="text-align: center;"> <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly â¤]"/> #Misskey</MkButton> </div> <FormSection> @@ -126,6 +126,12 @@ const patronsWithIcon = [{ }, { name: 'ã±ãƒ¼ã“', icon: 'https://misskey-hub.net/patrons/79c6602ffade489e8df2fcf2c2bc5d9d.jpg', +}, { + name: 'ã‚ã£ã»ãƒ¼â˜†', + icon: 'https://misskey-hub.net/patrons/d31d5d13924443a082f3da7966318a0a.jpg', +}, { + name: 'mollinaca', + icon: 'https://misskey-hub.net/patrons/ceb36b8f66e549bdadb3b90d5da62314.jpg', }]; const patrons = [ @@ -210,6 +216,7 @@ const patrons = [ 'ã‚ã‚玉', '氷月氷è¯é‡Œ', 'Ebise Lutica', + '巣黒るã„@リスケモ男ã®å¨˜VTuber!', ]; let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure')); diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index b054999303..8aae39cba1 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -12,7 +12,7 @@ <MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> <MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> - <MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu> + <MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu> </div> </MkSpacer> </div> @@ -23,7 +23,7 @@ </template> <script lang="ts" setup> -import { onMounted, onUnmounted, provide, watch } from 'vue'; +import { onActivated, onMounted, onUnmounted, provide, watch } from 'vue'; import { i18n } from '@/i18n'; import MkSuperMenu from '@/components/MkSuperMenu.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -144,6 +144,11 @@ const menuDef = $computed(() => [{ to: '/admin/settings', active: currentPage?.route.name === 'settings', }, { + icon: 'ti ti-shield', + text: i18n.ts.moderation, + to: '/admin/moderation', + active: currentPage?.route.name === 'moderation', + }, { icon: 'ti ti-mail', text: i18n.ts.emailServer, to: '/admin/email-settings', @@ -204,10 +209,23 @@ onMounted(() => { } }); +onActivated(() => { + narrow = el.offsetWidth < NARROW_THRESHOLD; + if (currentPage?.route.name == null && !narrow) { + router.push('/admin/overview'); + } +}); + onUnmounted(() => { ro.disconnect(); }); +watch(router.currentRef, (to) => { + if (to.route.path === "/admin" && to.child?.route.name == null && !narrow) { + router.replace('/admin/overview'); + } +}); + provideMetadataReceiver((info) => { if (info == null) { childInfo = null; diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue new file mode 100644 index 0000000000..7c2f04a9ab --- /dev/null +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -0,0 +1,73 @@ +<template> +<div> + <MkStickyContainer> + <template #header><XHeader :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <FormSuspense :p="init"> + <div class="_gaps_m"> + <FormSection first> + <div class="_gaps_m"> + <MkTextarea v-model="sensitiveWords"> + <template #label>{{ i18n.ts.sensitiveWords }}</template> + <template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template> + </MkTextarea> + </div> + </FormSection> + </div> + </FormSuspense> + </MkSpacer> + <template #footer> + <div :class="$style.footer"> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="16"> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + </MkSpacer> + </div> + </template> + </MkStickyContainer> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import XHeader from './_header_.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; +import FormSection from '@/components/form/section.vue'; +import FormSplit from '@/components/form/split.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import * as os from '@/os'; +import { fetchInstance } from '@/instance'; +import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import MkButton from '@/components/MkButton.vue'; + +let sensitiveWords: string = $ref(''); + +async function init() { + const meta = await os.api('admin/meta'); + sensitiveWords = meta.pinnedUsers.join('\n'); +} + +function save() { + os.apiWithDialog('admin/update-meta', { + sensitiveWords: sensitiveWords.split('\n'), + }).then(() => { + fetchInstance(); + }); +} + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.moderation, + icon: 'ti ti-shield', +}); +</script> + +<style lang="scss" module> +.footer { + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} +</style> diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue index 80e97fed93..509d329eb1 100644 --- a/packages/frontend/src/pages/admin/queue.vue +++ b/packages/frontend/src/pages/admin/queue.vue @@ -4,6 +4,8 @@ <MkSpacer :content-max="800"> <XQueue v-if="tab === 'deliver'" domain="deliver"/> <XQueue v-else-if="tab === 'inbox'" domain="inbox"/> + <br> + <MkButton @click="promoteAllQueues"><i class="ti ti-reload"></i> {{ i18n.ts.retryAllQueuesNow }}</MkButton> </MkSpacer> </MkStickyContainer> </template> @@ -15,6 +17,7 @@ import * as os from '@/os'; import * as config from '@/config'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import MkButton from '@/components/MkButton.vue'; let tab = $ref('deliver'); @@ -30,6 +33,18 @@ function clear() { }); } +function promoteAllQueues() { + os.confirm({ + type: 'warning', + title: i18n.ts.retryAllQueuesConfirmTitle, + text: i18n.ts.retryAllQueuesConfirmText, + }).then(({ canceled }) => { + if (canceled) return; + + os.apiWithDialog('admin/queue/promote', { type: tab }); + }); +} + const headerActions = $computed(() => [{ asFullButton: true, icon: 'ti ti-external-link', diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index ac6cca84c1..e6896237f8 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -55,6 +55,7 @@ if (props.id) { isPublic: false, asBadge: false, canEditMembersByModerator: false, + displayOrder: 0, policies: {}, }; } diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 2fb605f8c0..873ff02feb 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -17,6 +17,11 @@ <template #label>{{ i18n.ts._role.iconUrl }}</template> </MkInput> + <MkInput v-model="role.displayOrder" type="number"> + <template #label>{{ i18n.ts._role.displayOrder }}</template> + <template #caption>{{ i18n.ts._role.descriptionOfDisplayOrder }}</template> + </MkInput> + <MkSelect v-model="rolePermission" :readonly="readonly"> <template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template> <template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template> @@ -182,6 +187,26 @@ </div> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])"> + <template #label>{{ i18n.ts._role._options.canSearchNotes }}</template> + <template #suffix> + <span v-if="role.policies.canSearchNotes.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canSearchNotes.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canSearchNotes)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canSearchNotes.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canSearchNotes.value" :disabled="role.policies.canSearchNotes.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canSearchNotes.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])"> <template #label>{{ i18n.ts._role._options.driveCapacity }}</template> <template #suffix> @@ -444,6 +469,7 @@ const save = throttle(100, () => { description: role.description, color: role.color === '' ? null : role.color, iconUrl: role.iconUrl === '' ? null : role.iconUrl, + displayOrder: role.displayOrder, target: role.target, condFormula: role.condFormula, isAdministrator: role.isAdministrator, diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 25d8f3ad6e..a1e467edbd 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -7,7 +7,11 @@ <MkFolder> <template #label>{{ i18n.ts._role.baseRole }}</template> <div class="_gaps_s"> - <MkFolder> + <MkInput v-model="baseRoleQ" type="search"> + <template #prefix><i class="ti ti-search"></i></template> + </MkInput> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.rateLimitFactor, 'rateLimitFactor'])"> <template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template> <template #suffix>{{ Math.floor(policies.rateLimitFactor * 100) }}%</template> <MkRange :model-value="policies.rateLimitFactor * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => policies.rateLimitFactor = (v / 100)"> @@ -15,7 +19,7 @@ </MkRange> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])"> <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> <template #suffix>{{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> <MkSwitch v-model="policies.gtlAvailable"> @@ -23,7 +27,7 @@ </MkSwitch> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.ltlAvailable, 'ltlAvailable'])"> <template #label>{{ i18n.ts._role._options.ltlAvailable }}</template> <template #suffix>{{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> <MkSwitch v-model="policies.ltlAvailable"> @@ -31,7 +35,7 @@ </MkSwitch> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])"> <template #label>{{ i18n.ts._role._options.canPublicNote }}</template> <template #suffix>{{ policies.canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template> <MkSwitch v-model="policies.canPublicNote"> @@ -39,7 +43,7 @@ </MkSwitch> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])"> <template #label>{{ i18n.ts._role._options.canInvite }}</template> <template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template> <MkSwitch v-model="policies.canInvite"> @@ -47,7 +51,7 @@ </MkSwitch> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])"> <template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template> <template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template> <MkSwitch v-model="policies.canManageCustomEmojis"> @@ -55,7 +59,15 @@ </MkSwitch> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])"> + <template #label>{{ i18n.ts._role._options.canSearchNotes }}</template> + <template #suffix>{{ policies.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canSearchNotes"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])"> <template #label>{{ i18n.ts._role._options.driveCapacity }}</template> <template #suffix>{{ policies.driveCapacityMb }}MB</template> <MkInput v-model="policies.driveCapacityMb" type="number"> @@ -63,21 +75,21 @@ </MkInput> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])"> <template #label>{{ i18n.ts._role._options.pinMax }}</template> <template #suffix>{{ policies.pinLimit }}</template> <MkInput v-model="policies.pinLimit" type="number"> </MkInput> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])"> <template #label>{{ i18n.ts._role._options.antennaMax }}</template> <template #suffix>{{ policies.antennaLimit }}</template> <MkInput v-model="policies.antennaLimit" type="number"> </MkInput> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])"> <template #label>{{ i18n.ts._role._options.wordMuteMax }}</template> <template #suffix>{{ policies.wordMuteLimit }}</template> <MkInput v-model="policies.wordMuteLimit" type="number"> @@ -85,42 +97,42 @@ </MkInput> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])"> <template #label>{{ i18n.ts._role._options.webhookMax }}</template> <template #suffix>{{ policies.webhookLimit }}</template> <MkInput v-model="policies.webhookLimit" type="number"> </MkInput> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])"> <template #label>{{ i18n.ts._role._options.clipMax }}</template> <template #suffix>{{ policies.clipLimit }}</template> <MkInput v-model="policies.clipLimit" type="number"> </MkInput> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])"> <template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template> <template #suffix>{{ policies.noteEachClipsLimit }}</template> <MkInput v-model="policies.noteEachClipsLimit" type="number"> </MkInput> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])"> <template #label>{{ i18n.ts._role._options.userListMax }}</template> <template #suffix>{{ policies.userListLimit }}</template> <MkInput v-model="policies.userListLimit" type="number"> </MkInput> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])"> <template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template> <template #suffix>{{ policies.userEachUserListsLimit }}</template> <MkInput v-model="policies.userEachUserListsLimit" type="number"> </MkInput> </MkFolder> - <MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])"> <template #label>{{ i18n.ts._role._options.canHideAds }}</template> <template #suffix>{{ policies.canHideAds ? i18n.ts.yes : i18n.ts.no }}</template> <MkSwitch v-model="policies.canHideAds"> @@ -153,7 +165,7 @@ </template> <script lang="ts" setup> -import { computed, reactive } from 'vue'; +import { computed, reactive, ref } from 'vue'; import XHeader from './_header_.vue'; import MkInput from '@/components/MkInput.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -167,27 +179,10 @@ import { definePageMetadata } from '@/scripts/page-metadata'; import { instance } from '@/instance'; import { useRouter } from '@/router'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; - -const ROLE_POLICIES = [ - 'gtlAvailable', - 'ltlAvailable', - 'canPublicNote', - 'canInvite', - 'canManageCustomEmojis', - 'canHideAds', - 'driveCapacityMb', - 'pinLimit', - 'antennaLimit', - 'wordMuteLimit', - 'webhookLimit', - 'clipLimit', - 'noteEachClipsLimit', - 'userListLimit', - 'userEachUserListsLimit', - 'rateLimitFactor', -] as const; +import { ROLE_POLICIES } from '@/const'; const router = useRouter(); +const baseRoleQ = ref(''); const roles = await os.api('admin/roles/list'); @@ -196,6 +191,11 @@ for (const ROLE_POLICY of ROLE_POLICIES) { policies[ROLE_POLICY] = instance.policies[ROLE_POLICY]; } +function matchQuery(keywords: string[]): boolean { + if (baseRoleQ.value.trim().length === 0) return true; + return keywords.some(keyword => keyword.toLowerCase().includes(baseRoleQ.value.toLowerCase())); +} + async function updateBaseRole() { await os.apiWithDialog('admin/roles/update-default-policies', { policies, diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue new file mode 100644 index 0000000000..728ef3c0b1 --- /dev/null +++ b/packages/frontend/src/pages/ads.vue @@ -0,0 +1,25 @@ +<template> +<MkStickyContainer> + <template #header><MkPageHeader/></template> + + <MkSpacer :content-max="500"> + <div class="_gaps"> + <MkAd v-for="ad in instance.ads" :key="ad.id" :specify="ad"/> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { computed, watch } from 'vue'; +import * as os from '@/os'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import { i18n } from '@/i18n'; +import { instance } from '@/instance'; + +definePageMetadata({ + title: i18n.ts.ads, + icon: 'ti ti-ad', +}); +</script> + diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 65edb97e83..76f11faab8 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -46,7 +46,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import XChannelFollowButton from '@/components/MkChannelFollowButton.vue'; import * as os from '@/os'; import { useRouter } from '@/router'; -import { $i } from '@/account'; +import { $i, iAmModerator } from '@/account'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { deviceKind } from '@/scripts/device-kind'; @@ -90,21 +90,30 @@ function openPostForm() { }); } -const headerActions = $computed(() => channel && channel.userId ? [{ - icon: 'ti ti-share', - text: i18n.ts.share, - handler: async (): Promise<void> => { - navigator.share({ - title: channel.name, - text: channel.description, - url: `${url}/channels/${channel.id}`, - }); - }, -}, { - icon: 'ti ti-settings', - text: i18n.ts.edit, - handler: edit, -}] : null); +const headerActions = $computed(() => { + if (channel && channel.userId) { + const share = { + icon: 'ti ti-share', + text: i18n.ts.share, + handler: async (): Promise<void> => { + navigator.share({ + title: channel.name, + text: channel.description, + url: `${url}/channels/${channel.id}`, + }); + }, + }; + + const canEdit = ($i && $i.id === channel.userId) || iAmModerator; + return canEdit ? [share, { + icon: 'ti ti-settings', + text: i18n.ts.edit, + handler: edit, + }] : [share]; + } else { + return null; + } +}); const headerTabs = $computed(() => [{ key: 'overview', diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index d66088d33a..7515a9122a 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -7,6 +7,8 @@ <div v-if="clip.description" class="description"> <Mfm :text="clip.description" :is-note="false" :i="$i"/> </div> + <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> + <MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> <div class="user"> <MkAvatar :user="clip.user" class="avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/> </div> @@ -27,12 +29,14 @@ import { i18n } from '@/i18n'; import * as os from '@/os'; import { definePageMetadata } from '@/scripts/page-metadata'; import { url } from '@/config'; +import MkButton from '@/components/MkButton.vue'; const props = defineProps<{ clipId: string, }>(); let clip: misskey.entities.Clip = $ref<misskey.entities.Clip>(); +let favorited = $ref(false); const pagination = { endpoint: 'clips/notes' as const, limit: 10, @@ -47,12 +51,34 @@ watch(() => props.clipId, async () => { clip = await os.api('clips/show', { clipId: props.clipId, }); + favorited = clip.isFavorited; }, { immediate: true, }); provide('currentClipPage', $$(clip)); +function favorite() { + os.apiWithDialog('clips/favorite', { + clipId: props.clipId, + }).then(() => { + favorited = true; + }); +} + +async function unfavorite() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.unfavoriteConfirm, + }); + if (confirm.canceled) return; + os.apiWithDialog('clips/unfavorite', { + clipId: props.clipId, + }).then(() => { + favorited = false; + }); +} + const headerActions = $computed(() => clip && isOwned ? [{ icon: 'ti ti-pencil', text: i18n.ts.edit, diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 9be30f76a0..84bc153b71 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -22,6 +22,9 @@ <template #label>{{ i18n.ts.tags }}</template> <template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template> </MkInput> + <MkInput v-model="license"> + <template #label>{{ i18n.ts.license }}</template> + </MkInput> <MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </MkSpacer> @@ -45,6 +48,7 @@ let dialog = $ref(null); let name: string = $ref(props.emoji.name); let category: string = $ref(props.emoji.category); let aliases: string = $ref(props.emoji.aliases.join(' ')); +let license: string = $ref(props.emoji.license ?? ''); const emit = defineEmits<{ (ev: 'done', v: { deleted?: boolean, updated?: any }): void, @@ -61,6 +65,7 @@ async function update() { name, category, aliases: aliases.split(' '), + license: license === '' ? null : license, }); emit('done', { @@ -69,6 +74,7 @@ async function update() { name, category, aliases: aliases.split(' '), + license: license === '' ? null : license, }, }); diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index 0edc290801..bdd21b29ee 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -34,6 +34,17 @@ function menu(ev) { copyToClipboard(`:${props.emoji.name}:`); os.success(); }, + }, { + text: i18n.ts.info, + icon: 'ti ti-info-circle', + action: () => { + os.apiGet('emoji', { name: props.emoji.name }).then(res => { + os.alert({ + type: 'info', + text: `License: ${res.license}`, + }); + }); + }, }], ev.currentTarget ?? ev.target); } </script> diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue index 51177d079c..6ac469f7ba 100644 --- a/packages/frontend/src/pages/explore.roles.vue +++ b/packages/frontend/src/pages/explore.roles.vue @@ -1,5 +1,5 @@ <template> -<MkSpacer :content-max="1200"> +<MkSpacer :content-max="700"> <div class="_gaps_s"> <MkRolePreview v-for="role in roles" :key="role.id" :role="role" :for-moderation="false"/> </div> @@ -13,10 +13,8 @@ import * as os from '@/os'; let roles = $ref(); -os.api('roles/list', { - limit: 30, -}).then(res => { - roles = res.filter(x => x.target === 'manual'); +os.api('roles/list').then(res => { + roles = res.filter(x => x.target === 'manual').sort((a, b) => b.displayOrder - a.displayOrder); }); </script> diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 2b7fcf74e1..35edcc7cda 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -33,7 +33,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkInput from '@/components/MkInput.vue'; import { useRouter } from '@/router'; -const PRESET_DEFAULT = `/// @ 0.12.4 +const PRESET_DEFAULT = `/// @ 0.13.1 var name = "" @@ -51,7 +51,7 @@ Ui:render([ ]) `; -const PRESET_OMIKUJI = `/// @ 0.12.4 +const PRESET_OMIKUJI = `/// @ 0.13.1 // ユーザーã”ã¨ã«æ—¥æ›¿ã‚りã®ãŠã¿ãã˜ã®ãƒ—リセット // é¸æŠžè‚¢ @@ -94,7 +94,7 @@ Ui:render([ ]) `; -const PRESET_SHUFFLE = `/// @ 0.12.4 +const PRESET_SHUFFLE = `/// @ 0.13.1 // å·»ãæˆ»ã—å¯èƒ½ãªæ–‡å—シャッフルã®ãƒ—リセット let string = "ペペãƒãƒ³ãƒãƒ¼ãƒŽ" @@ -173,7 +173,7 @@ var cursor = 0 do() `; -const PRESET_QUIZ = `/// @ 0.12.4 +const PRESET_QUIZ = `/// @ 0.13.1 let title = '地ç†ã‚¯ã‚¤ã‚º' let qas = [{ @@ -286,7 +286,7 @@ qaEls.push(Ui:C:container({ Ui:render(qaEls) `; -const PRESET_TIMELINE = `/// @ 0.12.4 +const PRESET_TIMELINE = `/// @ 0.13.1 // APIリクエストを行ã„ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインを表示ã™ã‚‹ãƒ—リセット @fetch() { diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index 835dd0b54c..a51d1c78a4 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -18,12 +18,9 @@ <MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA> <p class="acct">@{{ acct(req.follower) }}</p> </div> - <div v-if="req.follower.description" class="description" :title="req.follower.description"> - <Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :plain="true" :nowrap="true"/> - </div> - <div class="actions"> - <button class="_button" @click="accept(req.follower)"><i class="ti ti-check"></i></button> - <button class="_button" @click="reject(req.follower)"><i class="ti ti-x"></i></button> + <div class="commands"> + <MkButton class="command" rounded primary @click="accept(req.follower)"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton> + <MkButton class="command" rounded danger @click="reject(req.follower)"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton> </div> </div> </div> @@ -37,6 +34,7 @@ <script lang="ts" setup> import { shallowRef, computed } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; +import MkButton from '@/components/MkButton.vue'; import { userPage, acct } from '@/filters/user'; import * as os from '@/os'; import { i18n } from '@/i18n'; @@ -90,13 +88,11 @@ definePageMetadata(computed(() => ({ display: flex; width: calc(100% - 54px); position: relative; + flex-wrap: wrap; + gap: 8px; > .name { - width: 45%; - - @media (max-width: 500px) { - width: 100%; - } + flex: 1 1 50%; > .name, > .acct { @@ -136,6 +132,11 @@ definePageMetadata(computed(() => ({ } } + > .commands { + display: flex; + gap: 8px; + } + > .actions { position: absolute; top: 0; diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index a79601f32f..4c23985f3b 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -1,25 +1,30 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="700"> - <div class="qtcaoidl"> - <MkButton primary class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <div v-if="tab === 'my'" class="_gaps"> + <MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list"> - <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin"> - <b>{{ item.name }}</b> - <div v-if="item.description" class="description">{{ item.description }}</div> + <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps"> + <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`"> + <MkClipPreview :clip="item"/> </MkA> </MkPagination> </div> + <div v-else-if="tab === 'favorites'" class="_gaps"> + <MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`"> + <MkClipPreview :clip="item"/> + </MkA> + </div> </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> -import { } from 'vue'; +import { watch } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; +import MkClipPreview from '@/components/MkClipPreview.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; @@ -29,8 +34,15 @@ const pagination = { limit: 10, }; +let tab = $ref('my'); +let favorites = $ref(); + const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>(); +watch($$(tab), async () => { + favorites = await os.api('clips/my-favorites'); +}); + async function create() { const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { @@ -66,7 +78,15 @@ function onClipDeleted() { const headerActions = $computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = $computed(() => [{ + key: 'my', + title: i18n.ts.myClips, + icon: 'ti ti-paperclip', +}, { + key: 'favorites', + title: i18n.ts.favorites, + icon: 'ti ti-heart', +}]); definePageMetadata({ title: i18n.ts.clip, @@ -78,23 +98,6 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.qtcaoidl { - > .add { - margin: 0 auto 16px auto; - } - - > .list { - > .item { - display: block; - padding: 16px; +<style lang="scss" module> - > .description { - margin-top: 8px; - padding-top: 8px; - border-top: solid 0.5px var(--divider); - } - } - } -} </style> diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 165e357ebd..45efe655fb 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -17,13 +17,11 @@ </div> <div v-if="clips && clips.length > 0" class="clips _margin"> <div class="title">{{ i18n.ts.clip }}</div> - <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin"> - <b>{{ item.name }}</b> - <div v-if="item.description" class="description">{{ item.description }}</div> - <div class="user"> - <MkAvatar :user="item.user" class="avatar" indicator link preview/> <MkUserName :user="item.user" :nowrap="false"/> - </div> - </MkA> + <div class="_gaps"> + <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`"> + <MkClipPreview :clip="item"/> + </MkA> + </div> </div> <MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="ti ti-chevron-down"></i></MkButton> </div> @@ -51,6 +49,7 @@ import * as os from '@/os'; import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; import { dateString } from '@/filters/date'; +import MkClipPreview from '@/components/MkClipPreview.vue'; const props = defineProps<{ noteId: string; @@ -178,27 +177,6 @@ definePageMetadata(computed(() => note ? { font-weight: bold; padding: 12px; } - - > .item { - display: block; - padding: 16px; - - > .description { - padding: 8px 0; - } - - > .user { - $height: 32px; - padding-top: 16px; - border-top: solid 0.5px var(--divider); - line-height: $height; - - > .avatar { - width: $height; - height: $height; - } - } - } } } } diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index da64a4c1e0..a5c7cdaa71 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -75,9 +75,11 @@ const headerActions = $computed(() => [tab === 'all' ? { const headerTabs = $computed(() => [{ key: 'all', title: i18n.ts.all, + icon: 'ti ti-point', }, { key: 'unread', title: i18n.ts.unread, + icon: 'ti ti-loader', }, { key: 'mentions', title: i18n.ts.mentions, diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 202244b34c..b26255ce61 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -75,6 +75,8 @@ import MkPagination from '@/components/MkPagination.vue'; import MkPagePreview from '@/components/MkPagePreview.vue'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { pageViewInterruptors } from '@/store'; +import { deepClone } from '@/scripts/clone'; const props = defineProps<{ pageName: string; @@ -97,8 +99,17 @@ function fetchPage() { os.api('pages/show', { name: props.pageName, username: props.username, - }).then(_page => { + }).then(async _page => { page = _page; + + // plugin + if (pageViewInterruptors.length > 0) { + let result = deepClone(_page); + for (const interruptor of pageViewInterruptors) { + result = await interruptor.handler(result); + } + page = result; + } }).catch(err => { error = err; }); diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue index 7e81cd2c0d..cc6f8cc0cc 100644 --- a/packages/frontend/src/pages/search.vue +++ b/packages/frontend/src/pages/search.vue @@ -1,26 +1,42 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :content-max="800"> - <MkInput v-model="searchQuery" :large="true" :autofocus="true" :debounce="true" type="search" style="margin-bottom: var(--margin);" @update:model-value="search()"> - <template #prefix><i class="ti ti-search"></i></template> - </MkInput> - <MkTab v-model="searchType" style="margin-bottom: var(--margin);" @update:model-value="search()"> - <option value="note">{{ i18n.ts.note }}</option> - <option value="user">{{ i18n.ts.user }}</option> - </MkTab> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer v-if="tab === 'note'" :content-max="800"> + <div v-if="notesSearchAvailable" class="_gaps"> + <div class="_gaps"> + <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search"> + <template #prefix><i class="ti ti-search"></i></template> + </MkInput> + <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> + </div> - <div v-if="searchType === 'note'"> - <MkNotes v-if="searchQuery" ref="notes" :pagination="notePagination"/> + <MkFoldableSection v-if="notePagination"> + <template #header>{{ i18n.ts.searchResult }}</template> + <MkNotes :key="key" :pagination="notePagination"/> + </MkFoldableSection> </div> <div v-else> - <MkRadios v-model="searchOrigin" style="margin-bottom: var(--margin);" @update:model-value="search()"> - <option value="combined">{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option value="remote">{{ i18n.ts.remote }}</option> - </MkRadios> + <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo> + </div> + </MkSpacer> + <MkSpacer v-else-if="tab === 'user'" :content-max="800"> + <div class="_gaps"> + <div class="_gaps"> + <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search"> + <template #prefix><i class="ti ti-search"></i></template> + </MkInput> + <MkRadios v-model="searchOrigin" @update:model-value="search()"> + <option value="combined">{{ i18n.ts.all }}</option> + <option value="local">{{ i18n.ts.local }}</option> + <option value="remote">{{ i18n.ts.remote }}</option> + </MkRadios> + <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> + </div> - <MkUserList v-if="searchQuery" ref="users" :pagination="userPagination"/> + <MkFoldableSection v-if="userPagination"> + <template #header>{{ i18n.ts.searchResult }}</template> + <MkUserList :key="key" :pagination="userPagination"/> + </MkFoldableSection> </div> </MkSpacer> </MkStickyContainer> @@ -31,14 +47,15 @@ import { computed, onMounted } from 'vue'; import MkNotes from '@/components/MkNotes.vue'; import MkUserList from '@/components/MkUserList.vue'; import MkInput from '@/components/MkInput.vue'; -import MkTab from '@/components/MkTab.vue'; import MkRadios from '@/components/MkRadios.vue'; +import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import * as os from '@/os'; -import { useRouter, mainRouter } from '@/router'; - -const router = useRouter(); +import MkFoldableSection from '@/components/MkFoldableSection.vue'; +import { $i } from '@/account'; +import { instance } from '@/instance'; +import MkInfo from '@/components/MkInfo.vue'; const props = defineProps<{ query: string; @@ -47,97 +64,60 @@ const props = defineProps<{ origin?: string; }>(); +let key = $ref(''); +let tab = $ref('note'); let searchQuery = $ref(''); -let searchType = $ref('note'); let searchOrigin = $ref('combined'); +let notePagination = $ref(); +let userPagination = $ref(); + +const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes)); onMounted(() => { + tab = props.type ?? 'note'; searchQuery = props.query ?? ''; - searchType = props.type ?? 'note'; searchOrigin = props.origin ?? 'combined'; - - if (searchQuery) { - search(); - } }); -const search = async () => { +async function search() { const query = searchQuery.toString().trim(); if (query == null || query === '') return; - if (query.startsWith('@') && !query.includes(' ')) { - mainRouter.push(`/${query}`); - return; - } - - if (query.startsWith('#')) { - mainRouter.push(`/tags/${encodeURIComponent(query.substr(1))}`); - return; + if (tab === 'note') { + notePagination = { + endpoint: 'notes/search', + limit: 10, + params: { + query: searchQuery, + channelId: props.channel, + }, + }; + } else if (tab === 'user') { + userPagination = { + endpoint: 'users/search', + limit: 10, + params: { + query: searchQuery, + origin: searchOrigin, + }, + }; } - // like 2018/03/12 - if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(query.replace(/-/g, '/'))) { - const date = new Date(query.replace(/-/g, '/')); - - // 日付ã—ã‹æŒ‡å®šã•れã¦ãªã„å ´åˆã€ä¾‹ãˆã° 2018/03/12 ãªã‚‰ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ - // 2018/03/12 ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を「å«ã‚€ã€çµæžœã«ãªã‚‹ã“ã¨ã‚’期待ã™ã‚‹ã¯ãšãªã®ã§ - // 23時間59分進ã‚ã‚‹(ãã®ã¾ã¾ã 㨠2018/03/12 00:00:00 「ã¾ã§ã€ã® - // çµæžœã«ãªã£ã¦ã—ã¾ã„ã€2018/03/12 ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„ã¯å«ã¾ã‚Œãªã„) - if (query.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) { - date.setHours(23, 59, 59, 999); - } - - // TODO - //v.$root.$emit('warp', date); - os.alert({ - icon: 'ti ti-history', - iconOnly: true, autoClose: true, - }); - return; - } - - if (query.startsWith('https://')) { - const promise = os.api('ap/show', { - uri: query, - }); - - os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); - - const res = await promise; - - if (res.type === 'User') { - mainRouter.push(`/@${res.object.username}@${res.object.host}`); - } else if (res.type === 'Note') { - mainRouter.push(`/notes/${res.object.id}`); - } - - return; - } - - window.history.replaceState('', '', `/search?q=${encodeURIComponent(query)}&type=${searchType}${searchType === 'user' ? `&origin=${searchOrigin}` : ''}`); -}; - -const notePagination = { - endpoint: 'notes/search' as const, - limit: 10, - params: computed(() => ({ - query: searchQuery, - channelId: props.channel, - })), -}; -const userPagination = { - endpoint: 'users/search' as const, - limit: 10, - params: computed(() => ({ - query: searchQuery, - origin: searchOrigin, - })), -}; + key = query; +} const headerActions = $computed(() => []); -const headerTabs = $computed(() => []); +const headerTabs = $computed(() => [{ + key: 'note', + title: i18n.ts.notes, + icon: 'ti ti-pencil', +}, { + key: 'user', + title: i18n.ts.users, + icon: 'ti ti-users', +}]); definePageMetadata(computed(() => ({ title: searchQuery ? i18n.t('searchWith', { q: searchQuery }) : i18n.ts.search, diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index a5eaae2bad..a58e74fe69 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -2,21 +2,12 @@ <div class=""> <FormSuspense :p="init"> <div class="_gaps"> - <MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton> - - <div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)"> - <div class="avatar"> - <MkAvatar :user="account" class="avatar"/> - </div> - <div class="body"> - <div class="name"> - <MkUserName :user="account"/> - </div> - <div class="acct"> - <MkAcct :user="account"/> - </div> - </div> + <div class="_buttons"> + <MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton> + <MkButton @click="init"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton> </div> + + <MkUserCardMini v-for="user in accounts" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/> </div> </FormSuspense> </div> @@ -30,9 +21,11 @@ import * as os from '@/os'; import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import type * as Misskey from 'misskey-js'; +import MkUserCardMini from '@/components/MkUserCardMini.vue'; const storedAccounts = ref<any>(null); -const accounts = ref<any>(null); +const accounts = ref<Misskey.entities.UserDetailed[]>([]); const init = async () => { getAccounts().then(accounts => { @@ -52,7 +45,7 @@ function menu(account, ev) { icon: 'ti ti-switch-horizontal', action: () => switchAccount(account), }, { - text: i18n.ts.remove, + text: i18n.ts.logout, icon: 'ti ti-trash', danger: true, action: () => removeAccount(account), @@ -69,23 +62,25 @@ function addAccount(ev) { }], ev.currentTarget ?? ev.target); } -function removeAccount(account) { - _removeAccount(account.id); +async function removeAccount(account) { + await _removeAccount(account.id); + accounts.value = accounts.value.filter(x => x.id !== account.id); } function addExistingAccount() { os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { - done: res => { - addAccounts(res.id, res.i); + done: async res => { + await addAccounts(res.id, res.i); os.success(); + init(); }, }, 'closed'); } function createAccount() { os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { - done: res => { - addAccounts(res.id, res.i); + done: async res => { + await addAccounts(res.id, res.i); switchAccountWithToken(res.i); }, }, 'closed'); @@ -111,32 +106,8 @@ definePageMetadata({ }); </script> -<style lang="scss" scoped> -.lcjjdxlm { - display: flex; - padding: 16px; - - > .avatar { - display: block; - flex-shrink: 0; - margin: 0 12px 0 0; - - > .avatar { - width: 50px; - height: 50px; - } - } - - > .body { - display: flex; - flex-direction: column; - justify-content: center; - width: calc(100% - 62px); - position: relative; - - > .name { - font-weight: bold; - } - } +<style lang="scss" module> +.user { + cursor: pointer; } </style> diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue new file mode 100644 index 0000000000..8178343bbb --- /dev/null +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -0,0 +1,156 @@ +<template> +<div class="_gaps"> + <MkSelect v-model="sortModeSelect"> + <template #label>{{ i18n.ts.sort }}</template> + <option v-for="x in sortOptions" :key="x.value" :value="x.value">{{ x.displayName }}</option> + </MkSelect> + <div v-if="!fetching"> + <MkPagination v-slot="{items}" :pagination="pagination"> + <div class="_gaps"> + <div + v-for="file in items" :key="file.id" + class="_button" + @click="$event => onClick($event, file)" + @contextmenu.stop="$event => onContextMenu($event, file)" + > + <div :class="$style.file"> + <div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div> + <MkDriveFileThumbnail :class="$style.fileThumbnail" :file="file" fit="contain"/> + <div :class="$style.fileBody"> + <div style="margin-bottom: 4px;"> + {{ file.name }} + </div> + <div> + <span style="margin-right: 1em;">{{ file.type }}</span> + <span>{{ bytes(file.size) }}</span> + </div> + <div> + <span>{{ i18n.ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> + </div> + <div v-if="sortModeSelect === 'sizeDesc'"> + <div :class="$style.meter"><div :class="$style.meterValue" :style="genUsageBar(file.size)"></div></div> + </div> + </div> + </div> + </div> + </div> + </MkPagination> + </div> + <div v-else> + <MkLoading/> + </div> +</div> +</template> + +<script setup lang="ts"> +import { computed, ref, watch } from 'vue'; +import tinycolor from 'tinycolor2'; +import * as os from '@/os'; +import MkPagination from '@/components/MkPagination.vue'; +import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; +import { i18n } from '@/i18n'; +import bytes from '@/filters/bytes'; +import { dateString } from '@/filters/date'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import MkSelect from '@/components/MkSelect.vue'; +import { getDriveFileMenu } from '@/scripts/get-drive-file-menu'; + +let sortMode = ref('+size'); +const pagination = { + endpoint: 'drive/files' as const, + limit: 10, + params: computed(() => ({ sort: sortMode.value })), +}; + +const sortOptions = [ + { value: 'sizeDesc', displayName: i18n.ts._drivecleaner.orderBySizeDesc }, + { value: 'createdAtAsc', displayName: i18n.ts._drivecleaner.orderByCreatedAtAsc }, +]; + +const capacity = ref<number>(0); +const usage = ref<number>(0); +const fetching = ref(true); +const sortModeSelect = ref('sizeDesc'); + +fetchDriveInfo(); + +watch(sortModeSelect, () => { + switch (sortModeSelect.value) { + case 'sizeDesc': + sortMode.value = '+size'; + fetchDriveInfo(); + break; + + case 'createdAtAsc': + sortMode.value = '-createdAt'; + fetchDriveInfo(); + break; + } +}); + +function fetchDriveInfo(): void { + fetching.value = true; + os.api('drive').then(info => { + capacity.value = info.capacity; + usage.value = info.usage; + fetching.value = false; + }); +} + +function genUsageBar(fsize: number): object { + return { + width: `${fsize / usage.value * 100}%`, + background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }), + }; +} + +function onClick(ev: MouseEvent, file) { + os.popupMenu(getDriveFileMenu(file), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); +} + +function onContextMenu(ev: MouseEvent, file): void { + os.contextMenu(getDriveFileMenu(file), ev); +} + +definePageMetadata({ + title: i18n.ts.drivecleaner, + icon: 'ti ti-trash', +}); +</script> + +<style lang="scss" module> +.file { + display: flex; + width: 100%; + box-sizing: border-box; + text-align: left; + align-items: center; + + &:hover { + color: var(--accent); + } +} + +.fileThumbnail { + width: 100px; + height: 100px; +} + +.fileBody { + margin-left: 0.3em; + padding: 8px; + flex: 1; +} + +.meter { + margin-top: 8px; + height: 12px; + background: rgba(0, 0, 0, 0.1); + overflow: clip; + border-radius: 999px; +} + +.meterValue { + height: 100%; +} +</style> diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index a23bdfe69e..d3fb422e01 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -32,6 +32,9 @@ <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> <template #suffixIcon><i class="ti ti-folder"></i></template> </FormLink> + <FormLink to="/settings/drive/cleaner"> + {{ i18n.ts.drivecleaner }} + </FormLink> <MkSwitch v-model="keepOriginalUploading"> <template #label>{{ i18n.ts.keepOriginalUploading }}</template> <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template> diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index a5619eab86..ae36466eec 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -7,7 +7,7 @@ <div v-if="!narrow || currentPage?.route.name == null" class="nav"> <div class="baaadecd"> <MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> - <MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu> + <MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu> </div> </div> <div v-if="!(narrow && currentPage?.route.name == null)" class="main"> @@ -135,6 +135,11 @@ const menuDef = computed(() => [{ to: '/settings/import-export', active: currentPage?.route.name === 'import-export', }, { + icon: 'ti ti-badges', + text: i18n.ts.roles, + to: '/settings/roles', + active: currentPage?.route.name === 'roles', + }, { icon: 'ti ti-planet-off', text: i18n.ts.instanceMute, to: '/settings/instance-mute', @@ -225,6 +230,12 @@ onUnmounted(() => { ro.disconnect(); }); +watch(router.currentRef, (to) => { + if (to.route.name === "settings" && to.child?.route.name == null && !narrow) { + router.replace('/settings/profile'); + } +}); + const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); provideMetadataReceiver((info) => { diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index a08308f0ce..3d0463f708 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -1,26 +1,95 @@ <template> <div class="_gaps_m"> - <MkTab v-model="tab" style="margin-bottom: var(--margin);"> + <MkTab v-model="tab"> + <option value="renoteMute">{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</option> <option value="mute">{{ i18n.ts.mutedUsers }}</option> <option value="block">{{ i18n.ts.blockedUsers }}</option> </MkTab> - <div v-if="tab === 'mute'"> - <MkPagination :pagination="mutingPagination" class="muting"> - <template #empty><FormInfo>{{ i18n.ts.noUsers }}</FormInfo></template> - <template #default="{items}"> - <FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)"> - <MkAcct :user="mute.mutee"/> - </FormLink> + + <div v-if="tab === 'renoteMute'"> + <MkPagination :pagination="renoteMutingPagination"> + <template #empty> + <div class="_fullinfo"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ i18n.ts.noUsers }}</div> + </div> + </template> + + <template #default="{ items }"> + <div class="_gaps_s"> + <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedRenoteMuteItems.includes(item.id) }]"> + <div :class="$style.userItemMain"> + <MkA :class="$style.userItemMainBody" :to="`/user-info/${item.mutee.id}`"> + <MkUserCardMini :user="item.mutee"/> + </MkA> + <button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ti ti-x"></i></button> + </div> + <div v-if="expandedRenoteMuteItems.includes(item.id)" :class="$style.userItemSub"> + <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> + </div> + </div> + </div> </template> </MkPagination> </div> - <div v-if="tab === 'block'"> - <MkPagination :pagination="blockingPagination" class="blocking"> - <template #empty><FormInfo>{{ i18n.ts.noUsers }}</FormInfo></template> - <template #default="{items}"> - <FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)"> - <MkAcct :user="block.blockee"/> - </FormLink> + + <div v-else-if="tab === 'mute'"> + <MkPagination :pagination="mutingPagination"> + <template #empty> + <div class="_fullinfo"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ i18n.ts.noUsers }}</div> + </div> + </template> + + <template #default="{ items }"> + <div class="_gaps_s"> + <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedMuteItems.includes(item.id) }]"> + <div :class="$style.userItemMain"> + <MkA :class="$style.userItemMainBody" :to="`/user-info/${item.mutee.id}`"> + <MkUserCardMini :user="item.mutee"/> + </MkA> + <button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ti ti-x"></i></button> + </div> + <div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub"> + <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> + <div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> + <div v-else>Period: {{ i18n.ts.indefinitely }}</div> + </div> + </div> + </div> + </template> + </MkPagination> + </div> + + <div v-else-if="tab === 'block'"> + <MkPagination :pagination="blockingPagination"> + <template #empty> + <div class="_fullinfo"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ i18n.ts.noUsers }}</div> + </div> + </template> + + <template #default="{ items }"> + <div class="_gaps_s"> + <div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]"> + <div :class="$style.userItemMain"> + <MkA :class="$style.userItemMainBody" :to="`/user-info/${item.blockee.id}`"> + <MkUserCardMini :user="item.blockee"/> + </MkA> + <button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.remove" @click="unblock(item.blockee, $event)"><i class="ti ti-x"></i></button> + </div> + <div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub"> + <div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div> + <div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> + <div v-else>Period: {{ i18n.ts.indefinitely }}</div> + </div> + </div> + </div> </template> </MkPagination> </div> @@ -36,8 +105,15 @@ import FormLink from '@/components/form/link.vue'; import { userPage } from '@/filters/user'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import * as os from '@/os'; -let tab = $ref('mute'); +let tab = $ref('renoteMute'); + +const renoteMutingPagination = { + endpoint: 'renote-mute/list' as const, + limit: 10, +}; const mutingPagination = { endpoint: 'mute/list' as const, @@ -49,6 +125,67 @@ const blockingPagination = { limit: 10, }; +let expandedRenoteMuteItems = $ref([]); +let expandedMuteItems = $ref([]); +let expandedBlockItems = $ref([]); + +async function unrenoteMute(user, ev) { + os.popupMenu([{ + text: i18n.ts.renoteUnmute, + icon: 'ti ti-x', + action: async () => { + await os.apiWithDialog('renote-mute/delete', { userId: user.id }); + //role.users = role.users.filter(u => u.id !== user.id); + }, + }], ev.currentTarget ?? ev.target); +} + +async function unmute(user, ev) { + os.popupMenu([{ + text: i18n.ts.unmute, + icon: 'ti ti-x', + action: async () => { + await os.apiWithDialog('mute/delete', { userId: user.id }); + //role.users = role.users.filter(u => u.id !== user.id); + }, + }], ev.currentTarget ?? ev.target); +} + +async function unblock(user, ev) { + os.popupMenu([{ + text: i18n.ts.unblock, + icon: 'ti ti-x', + action: async () => { + await os.apiWithDialog('blocking/delete', { userId: user.id }); + //role.users = role.users.filter(u => u.id !== user.id); + }, + }], ev.currentTarget ?? ev.target); +} + +async function toggleRenoteMuteItem(item) { + if (expandedRenoteMuteItems.includes(item.id)) { + expandedRenoteMuteItems = expandedRenoteMuteItems.filter(x => x !== item.id); + } else { + expandedRenoteMuteItems.push(item.id); + } +} + +async function toggleMuteItem(item) { + if (expandedMuteItems.includes(item.id)) { + expandedMuteItems = expandedMuteItems.filter(x => x !== item.id); + } else { + expandedMuteItems.push(item.id); + } +} + +async function toggleBlockItem(item) { + if (expandedBlockItems.includes(item.id)) { + expandedBlockItems = expandedBlockItems.filter(x => x !== item.id); + } else { + expandedBlockItems.push(item.id); + } +} + const headerActions = $computed(() => []); const headerTabs = $computed(() => []); @@ -58,3 +195,43 @@ definePageMetadata({ icon: 'ti ti-ban', }); </script> + +<style lang="scss" module> +.userItemMain { + display: flex; +} + +.userItemSub { + padding: 6px 12px; + font-size: 85%; + color: var(--fgTransparentWeak); +} + +.userItemMainBody { + flex: 1; + min-width: 0; + margin-right: 8px; + + &:hover { + text-decoration: none; + } +} + +.userToggle, +.remove { + width: 32px; + height: 32px; + align-self: center; +} + +.chevron { + display: block; + transition: transform 0.1s ease-out; +} + +.userItem.userItemOpend { + .chevron { + transform: rotateX(180deg); + } +} +</style> diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index ead551e7c4..b3b33b8026 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -1,9 +1,34 @@ <template> <div class="_gaps_m"> - <MkTextarea v-model="items" tall manual-save> + <FormSlot> <template #label>{{ i18n.ts.navbar }}</template> - <template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template> - </MkTextarea> + <MkContainer :show-header="false"> + <Sortable + v-model="items" + item-key="id" + :animation="150" + :handle="'.' + $style.itemHandle" + @start="e => e.item.classList.add('active')" + @end="e => e.item.classList.remove('active')" + > + <template #item="{element,index}"> + <div + v-if="element.type === '-' || navbarItemDef[element.type]" + :class="$style.item" + > + <button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button> + <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span> + <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button> + </div> + </template> + </Sortable> + </MkContainer> + </FormSlot> + <div class="_buttons"> + <MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton> + <MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + </div> <MkRadios v-model="menuDisplay"> <template #label>{{ i18n.ts.display }}</template> @@ -12,26 +37,30 @@ <option value="top">{{ i18n.ts._menuDisplay.top }}</option> <!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドãƒãƒ¼ã‚’完全ã«éš ã›ã‚‹ã‚ˆã†ã«ã™ã‚‹ã¨ã€åˆ¥é€”ãƒãƒ³ãƒãƒ¼ã‚¬ãƒ¼ãƒœã‚¿ãƒ³ã®ã‚ˆã†ãªã‚‚ã®ã‚’UIã«è¡¨ç¤ºã™ã‚‹å¿…è¦ãŒã‚りé¢å€’ --> </MkRadios> - - <MkButton danger @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> </div> </template> <script lang="ts" setup> -import { computed, ref, watch } from 'vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import { computed, defineAsyncComponent, ref, watch } from 'vue'; import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; +import FormSlot from '@/components/form/slot.vue'; +import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os'; import { navbarItemDef } from '@/navbar'; import { defaultStore } from '@/store'; import { unisonReload } from '@/scripts/unison-reload'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { deepClone } from '@/scripts/clone'; + +const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); -const items = ref(defaultStore.state.menu.join('\n')); +const items = ref(defaultStore.state.menu.map(x => ({ + id: Math.random().toString(), + type: x, +}))); -const split = computed(() => items.value.trim().split('\n').filter(x => x.trim() !== '')); const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); async function reloadAsk() { @@ -55,23 +84,28 @@ async function addItem() { }], }); if (canceled) return; - items.value = [...split.value, item].join('\n'); + items.value = [...items.value, { + id: Math.random().toString(), + type: item, + }]; +} + +function removeItem(index: number) { + items.value.splice(index, 1); } async function save() { - defaultStore.set('menu', split.value); + defaultStore.set('menu', items.value.map(x => x.type)); await reloadAsk(); } function reset() { - defaultStore.reset('menu'); - items.value = defaultStore.state.menu.join('\n'); + items.value = defaultStore.def.menu.default.map(x => ({ + id: Math.random().toString(), + type: x, + })); } -watch(items, async () => { - await save(); -}); - watch(menuDisplay, async () => { await reloadAsk(); }); @@ -85,3 +119,44 @@ definePageMetadata({ icon: 'ti ti-list', }); </script> + +<style lang="scss" module> +.item { + position: relative; + display: block; + line-height: 2.85rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: var(--navFg); +} + +.itemIcon { + position: relative; + width: 32px; + margin-right: 8px; +} + +.itemText { + position: relative; + font-size: 0.9em; +} + +.itemRemove { + position: absolute; + z-index: 10000; + width: 32px; + height: 32px; + color: #ff2a2a; + right: 8px; + opacity: 0.8; +} + +.itemHandle { + cursor: move; + width: 32px; + height: 32px; + margin: 0 8px; + opacity: 0.5; +} +</style> diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue index f23a338179..98063d6ff8 100644 --- a/packages/frontend/src/pages/settings/plugin.install.vue +++ b/packages/frontend/src/pages/settings/plugin.install.vue @@ -49,7 +49,7 @@ async function install() { text: 'No language version annotation found :(', }); return; - } else if (!lv.startsWith('0.12.')) { + } else if (!(lv.startsWith('0.12.') || lv.startsWith('0.13.'))) { os.alert({ type: 'error', text: `aiscript version '${lv}' is not supported :(`, diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 41563c441f..a5f6c11f89 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -64,12 +64,19 @@ </div> </MkFolder> + <MkSelect v-model="reactionAcceptance"> + <template #label>{{ i18n.ts.reactionAcceptance }}</template> + <option :value="null">{{ i18n.ts.all }}</option> + <option value="likeOnly">{{ i18n.ts.likeOnly }}</option> + <option value="likeOnlyForRemote">{{ i18n.ts.likeOnlyForRemote }}</option> + </MkSelect> + <MkSwitch v-model="profile.showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch> </div> </template> <script lang="ts" setup> -import { reactive, watch } from 'vue'; +import { computed, reactive, watch } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -85,6 +92,9 @@ import { $i } from '@/account'; import { langmap } from '@/scripts/langmap'; import { definePageMetadata } from '@/scripts/page-metadata'; import { claimAchievement } from '@/scripts/achievements'; +import { defaultStore } from '@/store'; + +const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance')); const profile = reactive({ name: $i.name, @@ -124,11 +134,17 @@ function saveFields() { function save() { os.apiWithDialog('i/update', { - name: profile.name ?? null, - description: profile.description ?? null, - location: profile.location ?? null, - birthday: profile.birthday ?? null, - lang: profile.lang ?? null, + // 空文å—列をnullã«ã—ãŸã„ã®ã§??ã¯ä½¿ã†ãª + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + name: profile.name || null, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + description: profile.description || null, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + location: profile.location || null, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + birthday: profile.birthday || null, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + lang: profile.lang || null, isBot: !!profile.isBot, isCat: !!profile.isCat, showTimelineReplies: !!profile.showTimelineReplies, diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue new file mode 100644 index 0000000000..ba510dced3 --- /dev/null +++ b/packages/frontend/src/pages/settings/roles.vue @@ -0,0 +1,56 @@ +<template> +<div class="_gaps_m"> + <FormSection first> + <template #label>{{ i18n.ts.rolesAssignedToMe }}</template> + <div class="_gaps_s"> + <MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :for-moderation="false"/> + </div> + </FormSection> + <FormSection> + <template #label>{{ i18n.ts._role.policies }}</template> + <div class="_gaps_s"> + <div v-for="policy in Object.keys($i.policies)" :key="policy"> + {{ policy }} ... {{ $i.policies[policy] }} + </div> + </div> + </FormSection> +</div> +</template> + +<script lang="ts" setup> +import { computed, reactive, watch } from 'vue'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import FormSplit from '@/components/form/split.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import FormSlot from '@/components/form/slot.vue'; +import FormSection from '@/components/form/section.vue'; +import * as os from '@/os'; +import { i18n } from '@/i18n'; +import { $i } from '@/account'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import { defaultStore } from '@/store'; +import MkRolePreview from '@/components/MkRolePreview.vue'; + +function save() { + os.apiWithDialog('i/update', { + + }); +} + +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.roles, + icon: 'ti ti-badges', +}); +</script> + +<style lang="scss" module> + +</style> diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue index 373af193d7..571f058240 100644 --- a/packages/frontend/src/pages/user-info.vue +++ b/packages/frontend/src/pages/user-info.vue @@ -262,14 +262,21 @@ async function updateRemoteUser() { } async function resetPassword() { - const { password } = await os.api('admin/reset-password', { - userId: user.id, - }); - - os.alert({ - type: 'success', - text: i18n.t('newPasswordIs', { password }), + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.resetPasswordConfirm, }); + if (confirm.canceled) { + return; + } else { + const { password } = await os.api('admin/reset-password', { + userId: user.id, + }); + os.alert({ + type: 'success', + text: i18n.t('newPasswordIs', { password }), + }); + } } async function toggleSuspend(v) { diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 02794175ae..7efaaebf5d 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -7,7 +7,7 @@ <!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> --> <div class="profile _gaps"> - <MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/> + <MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/> <div :key="user.id" class="main _panel"> <div class="banner-container" :style="style"> @@ -100,7 +100,7 @@ <XPhotos :key="user.id" :user="user"/> <XActivity :key="user.id" :user="user"/> </template> - <MkNotes :class="$style.tl" :no-gap="true" :pagination="pagination"/> + <MkNotes v-if="!disableNotes" :class="$style.tl" :no-gap="true" :pagination="pagination"/> </div> </div> <div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;"> @@ -137,7 +137,10 @@ const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); const props = withDefaults(defineProps<{ user: misskey.entities.UserDetailed; + /** Test only; MkNotes currently causes problems in vitest */ + disableNotes: boolean; }>(), { + disableNotes: false, }); const router = useRouter(); diff --git a/packages/frontend/src/pages/user/index.photos.vue b/packages/frontend/src/pages/user/index.photos.vue index 607082c1e4..85f6591eee 100644 --- a/packages/frontend/src/pages/user/index.photos.vue +++ b/packages/frontend/src/pages/user/index.photos.vue @@ -41,7 +41,7 @@ let images = $ref<{ function thumbnail(image: misskey.entities.DriveFile): string { return defaultStore.state.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) + ? getStaticImageUrl(image.url) : image.thumbnailUrl; } diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index a1a36480fd..9b6b01780c 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -1,7 +1,7 @@ import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; import { createAiScriptEnv } from '@/scripts/aiscript/api'; import { inputText } from '@/os'; -import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@/store'; +import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store'; const parser = new Parser(); const pluginContexts = new Map<string, Interpreter>(); @@ -80,6 +80,9 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s 'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => { registerNotePostInterruptor({ pluginId: opts.plugin.id, handler }); }), + 'Plugin:register_page_view_interruptor': values.FN_NATIVE(([handler]) => { + registerPageViewInterruptor({ pluginId: opts.plugin.id, handler }); + }), 'Plugin:open_url': values.FN_NATIVE(([url]) => { utils.assertString(url); window.open(url.value, '_blank'); @@ -156,3 +159,15 @@ function registerNotePostInterruptor({ pluginId, handler }): void { }, }); } + +function registerPageViewInterruptor({ pluginId, handler }): void { + pageViewInterruptors.push({ + handler: async (page) => { + const pluginContext = pluginContexts.get(pluginId); + if (!pluginContext) { + return; + } + return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(page)])); + }, + }); +} diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 70576688b1..c8077edd28 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -50,6 +50,10 @@ export const routes = [{ name: 'profile', component: page(() => import('./pages/settings/profile.vue')), }, { + path: '/roles', + name: 'roles', + component: page(() => import('./pages/settings/roles.vue')), + }, { path: '/privacy', name: 'privacy', component: page(() => import('./pages/settings/privacy.vue')), @@ -62,6 +66,10 @@ export const routes = [{ name: 'drive', component: page(() => import('./pages/settings/drive.vue')), }, { + path: '/drive/cleaner', + name: 'drive', + component: page(() => import('./pages/settings/drive-cleaner.vue')), + }, { path: '/notifications', name: 'notifications', component: page(() => import('./pages/settings/notifications.vue')), @@ -194,6 +202,9 @@ export const routes = [{ path: '/about-misskey', component: page(() => import('./pages/about-misskey.vue')), }, { + path: '/ads', + component: page(() => import('./pages/ads.vue')), +}, { path: '/theme-editor', component: page(() => import('./pages/theme-editor.vue')), loginRequired: true, @@ -381,6 +392,10 @@ export const routes = [{ name: 'settings', component: page(() => import('./pages/admin/settings.vue')), }, { + path: '/moderation', + name: 'moderation', + component: page(() => import('./pages/admin/moderation.vue')), + }, { path: '/email-settings', name: 'email-settings', component: page(() => import('./pages/admin/email-settings.vue')), diff --git a/packages/frontend/src/scripts/api.ts b/packages/frontend/src/scripts/api.ts index 5f34f5333e..97081d170f 100644 --- a/packages/frontend/src/scripts/api.ts +++ b/packages/frontend/src/scripts/api.ts @@ -5,7 +5,7 @@ import { $i } from '@/account'; export const pendingApiRequestsCount = ref(0); // Implements Misskey.api.ApiClient.request -export function api<E extends keyof Endpoints, P extends Endpoints[E]['req']>(endpoint: E, data: P = {} as any, token?: string | null | undefined): Promise<Endpoints[E]['res']> { +export function api<E extends keyof Endpoints, P extends Endpoints[E]['req']>(endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise<Endpoints[E]['res']> { pendingApiRequestsCount.value++; const onFinally = () => { @@ -26,6 +26,7 @@ export function api<E extends keyof Endpoints, P extends Endpoints[E]['req']>(en headers: { 'Content-Type': 'application/json', }, + signal, }).then(async (res) => { const body = res.status === 204 ? null : await res.json(); diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts new file mode 100644 index 0000000000..56ab516038 --- /dev/null +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -0,0 +1,93 @@ +import * as Misskey from 'misskey-js'; +import { defineAsyncComponent } from 'vue'; +import { i18n } from '@/i18n'; +import copyToClipboard from '@/scripts/copy-to-clipboard'; +import * as os from '@/os'; + +function rename(file: Misskey.entities.DriveFile) { + os.inputText({ + title: i18n.ts.renameFile, + placeholder: i18n.ts.inputNewFileName, + default: file.name, + }).then(({ canceled, result: name }) => { + if (canceled) return; + os.api('drive/files/update', { + fileId: file.id, + name: name, + }); + }); +} + +function describe(file: Misskey.entities.DriveFile) { + os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + default: file.comment != null ? file.comment : '', + file: file, + }, { + done: caption => { + os.api('drive/files/update', { + fileId: file.id, + comment: caption.length === 0 ? null : caption, + }); + }, + }, 'closed'); +} + +function toggleSensitive(file: Misskey.entities.DriveFile) { + os.api('drive/files/update', { + fileId: file.id, + isSensitive: !file.isSensitive, + }); +} + +function copyUrl(file: Misskey.entities.DriveFile) { + copyToClipboard(file.url); + os.success(); +} +/* +function addApp() { + alert('not implemented yet'); +} +*/ +async function deleteFile(file: Misskey.entities.DriveFile) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('driveFileDeleteConfirm', { name: file.name }), + }); + + if (canceled) return; + os.api('drive/files/delete', { + fileId: file.id, + }); +} + +export function getDriveFileMenu(file: Misskey.entities.DriveFile) { + return [{ + text: i18n.ts.rename, + icon: 'ti ti-forms', + action: rename, + }, { + text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, + icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-off', + action: toggleSensitive, + }, { + text: i18n.ts.describeFile, + icon: 'ti ti-text-caption', + action: describe, + }, null, { + text: i18n.ts.copyUrl, + icon: 'ti ti-link', + action: copyUrl, + }, { + type: 'a', + href: file.url, + target: '_blank', + text: i18n.ts.download, + icon: 'ti ti-download', + download: file.name, + }, null, { + text: i18n.ts.delete, + icon: 'ti ti-trash', + danger: true, + action: deleteFile, + }]; +} diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 5170ca4c8c..d7eb331183 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -53,6 +53,14 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router } } + async function toggleRenoteMute() { + os.apiWithDialog(user.isRenoteMuted ? 'renote-mute/delete' : 'renote-mute/create', { + userId: user.id, + }).then(() => { + user.isRenoteMuted = !user.isRenoteMuted; + }); + } + async function toggleBlock() { if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return; @@ -111,7 +119,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router icon: 'ti ti-mail', text: i18n.ts.sendMessage, action: () => { - os.post({ specified: user }); + os.post({ specified: user, initialText: `@${user.username} ` }); }, }, null, { type: 'parent', @@ -180,6 +188,10 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute, }, { + icon: user.isRenoteMuted ? 'ti ti-repeat' : 'ti ti-repeat-off', + text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute, + action: toggleRenoteMute, + }, { icon: 'ti ti-ban', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock, diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts new file mode 100644 index 0000000000..ce5b03fc38 --- /dev/null +++ b/packages/frontend/src/scripts/lookup.ts @@ -0,0 +1,41 @@ +import * as os from '@/os'; +import { i18n } from '@/i18n'; +import { mainRouter } from '@/router'; +import { Router } from '@/nirax'; + +export async function lookup(router?: Router) { + const _router = router ?? mainRouter; + + const { canceled, result: query } = await os.inputText({ + title: i18n.ts.lookup, + }); + if (canceled) return; + + if (query.startsWith('@') && !query.includes(' ')) { + _router.push(`/${query}`); + return; + } + + if (query.startsWith('#')) { + _router.push(`/tags/${encodeURIComponent(query.substr(1))}`); + return; + } + + if (query.startsWith('https://')) { + const promise = os.api('ap/show', { + uri: query, + }); + + os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); + + const res = await promise; + + if (res.type === 'User') { + _router.push(`/@${res.object.username}@${res.object.host}`); + } else if (res.type === 'Note') { + _router.push(`/notes/${res.object.id}`); + } + + return; + } +} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 2fe5bdcf8f..91ac14c06d 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -10,7 +10,10 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigi imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; } - return `${mustOrigin ? localProxy : instance.mediaProxy}/image.webp?${query({ + return `${mustOrigin ? localProxy : instance.mediaProxy}/${ + type === 'preview' ? 'preview.webp' + : 'image.webp' + }?${query({ url: imageUrl, fallback: '1', ...(type ? { [type]: '1' } : {}), diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index b08982facb..35fd007e64 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -72,8 +72,8 @@ export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioEle return audio; } -export function play(type: string) { - const sound = ColdDeviceStorage.get('sound_' + type as any); +export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') { + const sound = ColdDeviceStorage.get(`sound_${type}`); if (sound.type == null) return; playFile(sound.type, sound.volume); } diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 2766b434fc..3d87234f41 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -24,11 +24,16 @@ interface NotePostInterruptor { handler: (note: FIXME) => unknown; } +interface PageViewInterruptor { + handler: (page: Page) => unknown; +} + export const postFormActions: PostFormAction[] = []; export const userActions: UserAction[] = []; export const noteActions: NoteAction[] = []; export const noteViewInterruptors: NoteViewInterruptor[] = []; export const notePostInterruptors: NotePostInterruptor[] = []; +export const pageViewInterruptors: PageViewInterruptor[] = []; // TODO: ãれãžã‚Œã„ã¡ã„ã¡whereã¨ã‹defaultã¨ã„ã†ã‚ーを付ã‘ãªãゃã„ã‘ãªã„ã®å†—é•·ãªã®ã§ãªã‚“ã¨ã‹ã™ã‚‹(ãŸã 型定義ãŒé¢å€’ã«ãªã‚Šãã†) // ã‚ã¨ã€ç¾è¡Œã®å®šç¾©ã®ä»•æ–¹ãªã‚‰ã€ŒwhereãŒä½•ã§ã‚ã‚‹ã‹ã«é–¢ã‚らãšã‚ーåã®é‡è¤‡ä¸å¯ã€ã¨ã„ã†åˆ¶ç´„を付ã‘られるメリットもã‚ã‚‹ã‹ã‚‰ãã®ãƒ¡ãƒªãƒƒãƒˆã‚’引ãç¶™ãæ–¹æ³•も考ãˆãªã„ã¨ã„ã‘ãªã„ @@ -81,6 +86,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: ['ðŸ‘', 'â¤ï¸', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', 'ðŸ®'], }, + reactionAcceptance: { + where: 'account', + default: null, + }, mutedWords: { where: 'account', default: [], @@ -314,7 +323,7 @@ interface Watcher { import { miLocalStorage } from './local-storage'; import lightTheme from '@/themes/l-light.json5'; import darkTheme from '@/themes/d-green-lime.json5'; -import { Note, UserDetailed } from 'misskey-js/built/entities'; +import { Note, UserDetailed, Page } from 'misskey-js/built/entities'; export class ColdDeviceStorage { public static default = { @@ -324,8 +333,8 @@ export class ColdDeviceStorage { plugins: [] as Plugin[], mediaVolume: 0.5, sound_masterVolume: 0.5, - sound_note: { type: 'syuilo/n-aec', volume: 0.5 }, - sound_noteMy: { type: 'syuilo/n-cea', volume: 0.5 }, + sound_note: { type: 'syuilo/n-eca', volume: 0.5 }, + sound_noteMy: { type: 'syuilo/n-cea-4va', volume: 0.5 }, sound_notification: { type: 'syuilo/n-ea', volume: 0.5 }, sound_chat: { type: 'syuilo/pope1', volume: 0.5 }, sound_chatBg: { type: 'syuilo/waon', volume: 0.5 }, diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index a90ec6172f..eae4f0091c 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -29,6 +29,11 @@ export function openInstanceMenu(ev: MouseEvent) { icon: 'ti ti-chart-line', to: '/about#charts', }, null, { + type: 'link', + text: i18n.ts.ads, + icon: 'ti ti-ad', + to: '/ads', + }, { type: 'parent', text: i18n.ts.tools, icon: 'ti ti-tool', diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index a947e27e57..c23943d4db 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -8,12 +8,12 @@ <span style="margin-left: 8px;">{{ column.name }}</span> </template> - <div v-if="disabled" :class="$style.disabled"> + <div v-if="(((column.tl === 'local' || column.tl === 'social') && !isLocalTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled"> <p :class="$style.disabledTitle"> - <i class="ti ti-minus-circle"></i> - {{ $t('disabled-timeline.title') }} + <i class="ti ti-circle-minus"></i> + {{ i18n.ts._disabledTimeline.title }} </p> - <p :class="$style.disabledDescription">{{ $t('disabled-timeline.description') }}</p> + <p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p> </div> <MkTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')"/> </XColumn> @@ -27,6 +27,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os'; import { $i } from '@/account'; import { i18n } from '@/i18n'; +import { instance } from '@/instance'; const props = defineProps<{ column: Column; @@ -40,11 +41,16 @@ const emit = defineEmits<{ let disabled = $ref(false); +const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); +const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); + onMounted(() => { if (props.column.tl == null) { setType(); } else if ($i) { - disabled = false; // TODO + disabled = ( + (!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) || + (!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl))); } }); diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue index 7acf2140cf..e7f8819abd 100644 --- a/packages/frontend/src/widgets/WidgetActivity.vue +++ b/packages/frontend/src/widgets/WidgetActivity.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" class="mkw-activity data-cy-mkw-activity"> +<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" data-cy-mkw-activity class="mkw-activity"> <template #icon><i class="ti ti-chart-line"></i></template> <template #header>{{ i18n.ts._widgets.activity }}</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template> diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue index cb055a56f6..37326ee981 100644 --- a/packages/frontend/src/widgets/WidgetAichan.vue +++ b/packages/frontend/src/widgets/WidgetAichan.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-aichan data-cy-mkw-aichan"> +<MkContainer :naked="widgetProps.transparent" :show-header="false" data-cy-mkw-aichan class="mkw-aichan"> <iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe> </MkContainer> </template> diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index 33218a110b..947dbe5e77 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-aiscript data-cy-mkw-aiscript"> +<MkContainer :show-header="widgetProps.showHeader" data-cy-mkw-aiscript class="mkw-aiscript"> <template #icon><i class="ti ti-terminal-2"></i></template> <template #header>{{ i18n.ts._widgets.aiscript }}</template> diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue index 462f1e5a5d..9eee9680db 100644 --- a/packages/frontend/src/widgets/WidgetButton.vue +++ b/packages/frontend/src/widgets/WidgetButton.vue @@ -1,5 +1,5 @@ <template> -<div class="mkw-button data-cy-mkw-button"> +<div data-cy-mkw-button class="mkw-button"> <MkButton :primary="widgetProps.colored" full @click="run"> {{ widgetProps.label }} </MkButton> diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index 083d8588af..de2e4b179d 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -1,5 +1,5 @@ <template> -<div :class="[$style.root, { _panel: !widgetProps.transparent }]" class="data-cy-mkw-calendar"> +<div :class="[$style.root, { _panel: !widgetProps.transparent }]" data-cy-mkw-calendar> <div :class="[$style.calendar, { [$style.isHoliday]: isHoliday }]"> <p :class="$style.monthAndYear"> <span :class="$style.year">{{ $t('yearX', { year }) }}</span> diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue index ecbb03b570..ebd73cb9f5 100644 --- a/packages/frontend/src/widgets/WidgetClock.vue +++ b/packages/frontend/src/widgets/WidgetClock.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-clock data-cy-mkw-clock"> +<MkContainer :naked="widgetProps.transparent" :show-header="false" data-cy-mkw-clock class="mkw-clock"> <div class="vubelbmv" :class="widgetProps.size"> <div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace label a abbrev">{{ tzAbbrev }}</div> <MkAnalogClock diff --git a/packages/frontend/src/widgets/WidgetDigitalClock.vue b/packages/frontend/src/widgets/WidgetDigitalClock.vue index 1780a1c8d2..cdd9c3a401 100644 --- a/packages/frontend/src/widgets/WidgetDigitalClock.vue +++ b/packages/frontend/src/widgets/WidgetDigitalClock.vue @@ -1,5 +1,5 @@ <template> -<div class="data-cy-mkw-digitalClock _monospace" :class="[$style.root, { _panel: !widgetProps.transparent }]" :style="{ fontSize: `${widgetProps.fontSize}em` }"> +<div data-cy-mkw-digitalClock class="_monospace" :class="[$style.root, { _panel: !widgetProps.transparent }]" :style="{ fontSize: `${widgetProps.fontSize}em` }"> <div v-if="widgetProps.showLabel" :class="$style.label">{{ tzAbbrev }}</div> <div> <MkDigitalClock :show-ms="widgetProps.showMs" :offset="tzOffset"/> diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index a8095acf65..7dcd5cb42e 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" class="mkw-federation data-cy-mkw-federation"> +<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" data-cy-mkw-federation class="mkw-federation"> <template #icon><i class="ti ti-whirl"></i></template> <template #header>{{ i18n.ts._widgets.federation }}</template> diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index 69912e21f7..84043cf13f 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -1,5 +1,5 @@ <template> -<div class="mkw-jobQueue data-cy-mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }"> +<div data-cy-mkw-jobQueue class="mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }"> <div class="inbox"> <div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="ti ti-alert-triangle icon"></i></div> <div class="values"> diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue index 149d20af47..959cf776ad 100644 --- a/packages/frontend/src/widgets/WidgetMemo.vue +++ b/packages/frontend/src/widgets/WidgetMemo.vue @@ -1,10 +1,10 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-memo data-cy-mkw-memo"> +<MkContainer :show-header="widgetProps.showHeader" data-cy-mkw-memo class="mkw-memo"> <template #icon><i class="ti ti-note"></i></template> <template #header>{{ i18n.ts._widgets.memo }}</template> <div :class="$style.root"> - <textarea v-model="text" :class="$style.textarea" :placeholder="i18n.ts.placeholder" @input="onChange"></textarea> + <textarea v-model="text" :style="`height: ${widgetProps.height}px;`" :class="$style.textarea" :placeholder="i18n.ts.placeholder" @input="onChange"></textarea> <button :class="$style.save" :disabled="!changed" class="_buttonPrimary" @click="saveMemo">{{ i18n.ts.save }}</button> </div> </MkContainer> @@ -25,6 +25,10 @@ const widgetPropsDef = { type: 'boolean' as const, default: true, }, + height: { + type: 'number' as const, + default: 100, + }, }; type WidgetProps = GetFormResultType<typeof widgetPropsDef>; diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue index 63400b09d0..661f68b278 100644 --- a/packages/frontend/src/widgets/WidgetNotifications.vue +++ b/packages/frontend/src/widgets/WidgetNotifications.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true" class="mkw-notifications data-cy-mkw-notifications"> +<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true" data-cy-mkw-notifications class="mkw-notifications"> <template #icon><i class="ti ti-bell"></i></template> <template #header>{{ i18n.ts.notifications }}</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ti ti-settings"></i></button></template> diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index 7949fc4a93..44e073545d 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -1,5 +1,5 @@ <template> -<div class="mkw-onlineUsers data-cy-mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }"> +<div data-cy-mkw-onlineUsers class="mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }"> <I18n v-if="onlineUsersCount" :src="i18n.ts.onlineUsersCount" text-tag="span" class="text"> <template #n><b>{{ number(onlineUsersCount) }}</b></template> </I18n> diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index 8746ababbb..716bbb4274 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" class="mkw-photos data-cy-mkw-photos"> +<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" data-cy-mkw-photos class="mkw-photos"> <template #icon><i class="ti ti-camera"></i></template> <template #header>{{ i18n.ts._widgets.photos }}</template> @@ -67,7 +67,7 @@ const onDriveFileCreated = (file) => { const thumbnail = (image: any): string => { return defaultStore.state.disableShowingAnimatedImages - ? getStaticImageUrl(image.thumbnailUrl) + ? getStaticImageUrl(image.url) : image.thumbnailUrl; }; diff --git a/packages/frontend/src/widgets/WidgetPostForm.vue b/packages/frontend/src/widgets/WidgetPostForm.vue index 9953bca65f..7a96b00217 100644 --- a/packages/frontend/src/widgets/WidgetPostForm.vue +++ b/packages/frontend/src/widgets/WidgetPostForm.vue @@ -1,5 +1,5 @@ <template> -<MkPostForm class="_panel mkw-post-form data-cy-mkw-postForm" :fixed="true" :autofocus="false"/> +<MkPostForm data-cy-mkw-postForm class="_panel mkw-post-form" :fixed="true" :autofocus="false"/> </template> <script lang="ts" setup> diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index 965bb89153..18fa2e2c22 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-rss data-cy-mkw-rss"> +<MkContainer :show-header="widgetProps.showHeader" data-cy-mkw-rss class="mkw-rss"> <template #icon><i class="ti ti-rss"></i></template> <template #header>RSS</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ti ti-settings"></i></button></template> diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index ffb77b281a..22a0024271 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -1,5 +1,5 @@ <template> -<div class="kvausudm _panel mkw-slideshow data-cy-mkw-slideshow" :style="{ height: widgetProps.height + 'px' }"> +<div data-cy-mkw-slideshow class="kvausudm _panel mkw-slideshow" :style="{ height: widgetProps.height + 'px' }"> <div @click="choose"> <p v-if="widgetProps.folderId == null"> {{ i18n.ts.folder }} diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index d6be6532a6..0f6f25b0a9 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" class="mkw-timeline data-cy-mkw-timeline"> +<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" data-cy-mkw-timeline class="mkw-timeline"> <template #icon> <i v-if="widgetProps.src === 'home'" class="ti ti-home"></i> <i v-else-if="widgetProps.src === 'local'" class="ti ti-planet"></i> @@ -15,7 +15,14 @@ </button> </template> - <div> + <div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social') && !isLocalTimelineAvailable) || (widgetProps.src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled"> + <p :class="$style.disabledTitle"> + <i class="ti ti-minus"></i> + {{ i18n.ts._disabledTimeline.title }} + </p> + <p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p> + </div> + <div v-else> <MkTimeline :key="widgetProps.src === 'list' ? `list:${widgetProps.list.id}` : widgetProps.src === 'antenna' ? `antenna:${widgetProps.antenna.id}` : widgetProps.src" :src="widgetProps.src" :list="widgetProps.list ? widgetProps.list.id : null" :antenna="widgetProps.antenna ? widgetProps.antenna.id : null"/> </div> </MkContainer> @@ -29,8 +36,12 @@ import * as os from '@/os'; import MkContainer from '@/components/MkContainer.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import { i18n } from '@/i18n'; +import { $i } from '@/account'; +import { instance } from '@/instance'; const name = 'timeline'; +const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); +const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); const widgetPropsDef = { showHeader: { @@ -128,3 +139,17 @@ defineExpose<WidgetComponentExpose>({ id: props.widget ? props.widget.id : null, }); </script> + +<style lang="scss" module> +.disabled { + text-align: center; +} + +.disabledTitle { + margin: 16px; +} + +.disabledDescription { + font-size: 90%; +} +</style> diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index 1423ae076c..fc8a310ece 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -1,5 +1,5 @@ <template> -<MkContainer :show-header="widgetProps.showHeader" class="mkw-trends data-cy-mkw-trends"> +<MkContainer :show-header="widgetProps.showHeader" data-cy-mkw-trends class="mkw-trends"> <template #icon><i class="ti ti-hash"></i></template> <template #header>{{ i18n.ts._widgets.trends }}</template> diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue index 72c88d9a00..357d0ab78b 100644 --- a/packages/frontend/src/widgets/server-metric/index.vue +++ b/packages/frontend/src/widgets/server-metric/index.vue @@ -4,7 +4,7 @@ <template #header>{{ i18n.ts._widgets.serverMetric }}</template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template> - <div v-if="meta" class="mkw-serverMetric data-cy-mkw-serverMetric"> + <div v-if="meta" data-cy-mkw-serverMetric class="mkw-serverMetric"> <XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/> <XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/> <XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/> diff --git a/packages/frontend/test/home.test.ts b/packages/frontend/test/home.test.ts new file mode 100644 index 0000000000..f3eddcd371 --- /dev/null +++ b/packages/frontend/test/home.test.ts @@ -0,0 +1,58 @@ +import { describe, test, assert, afterEach } from 'vitest'; +import { render, cleanup, type RenderResult } from '@testing-library/vue'; +import './init'; +import type * as misskey from 'misskey-js'; +import { directives } from '@/directives'; +import { components } from '@/components/index'; +import XHome from '@/pages/user/home.vue'; + +describe('XHome', () => { + const renderHome = (user: Partial<misskey.entities.UserDetailed>): RenderResult => { + return render(XHome, { + props: { user, disableNotes: true }, + global: { directives, components }, + }); + }; + + afterEach(() => { + cleanup(); + }); + + test('Should render the remote caution when user.host exists', async () => { + const home = renderHome({ + id: 'blobcat', + name: 'blobcat', + host: 'example.com', + uri: 'https://example.com/@user', + url: 'https://example.com/@user/profile', + roles: [], + createdAt: '1970-01-01T00:00:00.000Z', + fields: [], + pinnedNotes: [], + }); + + const anchor = home.container.querySelector<HTMLAnchorElement>('a[href^="https://example.com/"]'); + assert.exists(anchor, 'anchor to the remote exists'); + assert.strictEqual(anchor?.href, 'https://example.com/@user/profile'); + + assert.ok(anchor?.parentElement?.classList.contains('warn'), 'the parent is a warning'); + }); + + test('The remote caution should fall back to uri if url is null', async () => { + const home = renderHome({ + id: 'blobcat', + name: 'blobcat', + host: 'example.com', + uri: 'https://example.com/@user', + url: null, + roles: [], + createdAt: '1970-01-01T00:00:00.000Z', + fields: [], + pinnedNotes: [], + }); + + const anchor = home.container.querySelector<HTMLAnchorElement>('a[href^="https://example.com/"]'); + assert.exists(anchor, 'anchor to the remote exists'); + assert.strictEqual(anchor?.href, 'https://example.com/@user'); + }); +}); diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts new file mode 100644 index 0000000000..295107e143 --- /dev/null +++ b/packages/frontend/test/init.ts @@ -0,0 +1,22 @@ +import { vi } from 'vitest'; +import createFetchMock from 'vitest-fetch-mock'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +// Set i18n +import locales from '../../../locales'; +import { updateI18n } from '@/i18n'; +updateI18n(locales['en-US']); + +// XXX: misskey-js panics if WebSocket is not defined +vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; }); + +// XXX: defaultStore somehow becomes undefined in vitest? +vi.mock('@/store.js', () => { + return { + defaultStore: { + state: {}, + }, + }; +}); diff --git a/packages/frontend/test/note.test.ts b/packages/frontend/test/note.test.ts new file mode 100644 index 0000000000..f7c47ec100 --- /dev/null +++ b/packages/frontend/test/note.test.ts @@ -0,0 +1,81 @@ +import { describe, test, assert, afterEach } from 'vitest'; +import { render, cleanup, type RenderResult } from '@testing-library/vue'; +import './init'; +import type { DriveFile } from 'misskey-js/built/entities'; +import { directives } from '@/directives'; +import MkMediaImage from '@/components/MkMediaImage.vue'; + +describe('MkMediaImage', () => { + const renderMediaImage = (image: Partial<DriveFile>): RenderResult => { + return render(MkMediaImage, { + props: { image }, + global: { directives }, + }); + }; + + afterEach(() => { + cleanup(); + }); + + test('Attaching JPG should show no indicator', async () => { + const mkMediaImage = renderMediaImage({ + type: 'image/jpeg', + }); + const [gif, alt] = await Promise.all([ + mkMediaImage.queryByText('GIF'), + mkMediaImage.queryByText('ALT'), + ]); + assert.ok(!gif); + assert.ok(!alt); + }); + + test('Attaching GIF should show a GIF indicator', async () => { + const mkMediaImage = renderMediaImage({ + type: 'image/gif', + }); + const [gif, alt] = await Promise.all([ + mkMediaImage.queryByText('GIF'), + mkMediaImage.queryByText('ALT'), + ]); + assert.ok(gif); + assert.ok(!alt); + }); + + test('Attaching APNG should show a GIF indicator', async () => { + const mkMediaImage = renderMediaImage({ + type: 'image/apng', + }); + const [gif, alt] = await Promise.all([ + mkMediaImage.queryByText('GIF'), + mkMediaImage.queryByText('ALT'), + ]); + assert.ok(gif); + assert.ok(!alt); + }); + + test('Attaching image with an alt message should show an ALT indicator', async () => { + const mkMediaImage = renderMediaImage({ + type: 'image/png', + comment: 'Misskeyã®ãƒã‚´ã§ã™', + }); + const [gif, alt] = await Promise.all([ + mkMediaImage.queryByText('GIF'), + mkMediaImage.queryByText('ALT'), + ]); + assert.ok(!gif); + assert.ok(alt); + }); + + test('Attaching GIF image with an alt message should show a GIF and an ALT indicator', async () => { + const mkMediaImage = renderMediaImage({ + type: 'image/gif', + comment: 'Misskeyã®ãƒã‚´ã§ã™', + }); + const [gif, alt] = await Promise.all([ + mkMediaImage.queryByText('GIF'), + mkMediaImage.queryByText('ALT'), + ]); + assert.ok(gif); + assert.ok(alt); + }); +}); diff --git a/packages/frontend/test/tsconfig.json b/packages/frontend/test/tsconfig.json new file mode 100644 index 0000000000..1424fdbdfb --- /dev/null +++ b/packages/frontend/test/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "allowJs": true, + "noEmitOnError": false, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedParameters": false, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "declaration": false, + "sourceMap": true, + "target": "es2021", + "module": "es2020", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "removeComments": false, + "noLib": false, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": "./", + "paths": { + "@/*": ["../src/*"] + }, + "typeRoots": [ + "../node_modules/@types", + ], + "lib": [ + "esnext", + "dom" + ], + "types": ["node"] + }, + "compileOnSave": false, + "include": [ + "./**/*.ts", + "../src/**/*.vue", + ] +} diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts new file mode 100644 index 0000000000..205982a40a --- /dev/null +++ b/packages/frontend/test/url-preview.test.ts @@ -0,0 +1,140 @@ +import { describe, test, assert, afterEach } from 'vitest'; +import { render, cleanup, type RenderResult } from '@testing-library/vue'; +import './init'; +import type { summaly } from 'summaly'; +import { directives } from '@/directives'; +import MkUrlPreview from '@/components/MkUrlPreview.vue'; + +type SummalyResult = Awaited<ReturnType<typeof summaly>>; + +describe('MkMediaImage', () => { + const renderPreviewBy = async (summary: Partial<SummalyResult>): Promise<RenderResult> => { + if (!summary.player) { + summary.player = { + url: null, + width: null, + height: null, + allow: [], + }; + } + + fetchMock.mockOnceIf(/^\/url?/, () => { + return { + status: 200, + body: JSON.stringify(summary), + }; + }); + + const result = render(MkUrlPreview, { + props: { url: summary.url }, + global: { directives }, + }); + + await new Promise<void>(resolve => { + const observer = new MutationObserver(() => { + resolve(); + observer.disconnect(); + }); + observer.observe(result.container, { childList: true, subtree: true }); + }); + + return result; + }; + + const renderAndOpenPreview = async (summary: Partial<SummalyResult>): Promise<HTMLIFrameElement | null> => { + const mkUrlPreview = await renderPreviewBy(summary); + const buttons = mkUrlPreview.getAllByRole('button'); + buttons[0].click(); + // Wait for the click event to be fired + await Promise.resolve(); + + return mkUrlPreview.container.querySelector('iframe'); + }; + + afterEach(() => { + fetchMock.resetMocks(); + cleanup(); + }); + + test('Should render the description', async () => { + const mkUrlPreview = await renderPreviewBy({ + url: 'https://example.local', + description: 'Mocked description', + }); + mkUrlPreview.getByText('Mocked description'); + }); + + test('Having a player should render a button', async () => { + const mkUrlPreview = await renderPreviewBy({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: null, + allow: [], + }, + }); + const buttons = mkUrlPreview.getAllByRole('button'); + assert.strictEqual(buttons.length, 2, 'two buttons'); + }); + + test('Having a player should setup the iframe', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: null, + allow: [], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.src, 'https://example.local/player?autoplay=1&auto_play=1'); + assert.strictEqual( + iframe?.sandbox.toString(), + 'allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin', + ); + }); + + test('Having a player with `allow` field should set permissions', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: null, + allow: ['fullscreen', 'web-share'], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.allow, 'fullscreen;web-share'); + }); + + test('Having a player width should keep the fixed aspect ratio', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: 400, + height: 200, + allow: [], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.parentElement?.style.paddingTop, '50%'); + }); + + test('Having a player width should keep the fixed height', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: 200, + allow: [], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.parentElement?.style.paddingTop, '200px'); + }); +}); diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 89b6dbde25..a90ee55268 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -1,6 +1,7 @@ import path from 'path'; import pluginVue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; +import { configDefaults as vitestConfigDefaults } from 'vitest/config'; import locales from '../../locales'; import meta from '../../package.json'; @@ -16,10 +17,10 @@ const hash = (str: string, seed = 0): number => { h1 = Math.imul(h1 ^ ch, 2654435761); h2 = Math.imul(h2 ^ ch, 1597334677); } - + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); - + return 4294967296 * (2097151 & h2) + (h1 >>> 0); }; @@ -28,12 +29,12 @@ function toBase62(n: number): string { if (n === 0) { return '0'; } - let result = ''; + let result = ''; while (n > 0) { result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result; n = Math.floor(n / BASE62_DIGITS.length); } - + return result; } @@ -110,5 +111,15 @@ export default defineConfig(({ command, mode }) => { sourcemap: process.env.NODE_ENV === 'development', reportCompressedSize: false, }, + + test: { + environment: 'happy-dom', + deps: { + inline: [ + // XXX: misskey-dev/browser-image-resizer has no "type": "module" + 'browser-image-resizer', + ], + }, + }, }; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17be01e45c..f56f77dc97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,8 +11,8 @@ importers: '@tensorflow/tfjs-core': 4.2.0 '@types/gulp': 4.0.10 '@types/gulp-rename': 2.0.1 - '@typescript-eslint/eslint-plugin': 5.53.0 - '@typescript-eslint/parser': 5.53.0 + '@typescript-eslint/eslint-plugin': 5.54.1 + '@typescript-eslint/parser': 5.54.1 cross-env: 7.0.3 cypress: 12.7.0 eslint: 8.35.0 @@ -23,7 +23,7 @@ importers: gulp-replace: 1.1.4 gulp-terser: 2.1.0 js-yaml: 4.1.0 - start-server-and-test: 1.15.4 + start-server-and-test: 2.0.0 typescript: 4.9.5 dependencies: execa: 5.1.1 @@ -39,45 +39,45 @@ importers: devDependencies: '@types/gulp': 4.0.10 '@types/gulp-rename': 2.0.1 - '@typescript-eslint/eslint-plugin': 5.53.0_cjo54hduev4bqhpjw5znwiokqu - '@typescript-eslint/parser': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu + '@typescript-eslint/eslint-plugin': 5.54.1_mlk7dnz565t663n4razh6a6v6i + '@typescript-eslint/parser': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu cross-env: 7.0.3 cypress: 12.7.0 eslint: 8.35.0 - start-server-and-test: 1.15.4 + start-server-and-test: 2.0.0 packages/backend: specifiers: - '@bull-board/api': 4.12.1 - '@bull-board/fastify': 4.12.1 - '@bull-board/ui': 4.12.1 + '@bull-board/api': 5.0.0 + '@bull-board/fastify': 5.0.0 + '@bull-board/ui': 5.0.0 '@discordapp/twemoji': 14.0.2 '@fastify/accepts': 4.1.0 '@fastify/cookie': 8.3.0 '@fastify/cors': 8.2.0 '@fastify/http-proxy': 8.4.0 - '@fastify/multipart': 7.4.1 + '@fastify/multipart': 7.4.2 '@fastify/static': 6.9.0 '@fastify/view': 7.4.1 - '@jest/globals': 29.4.3 + '@jest/globals': 29.5.0 '@nestjs/common': 9.3.9 '@nestjs/core': 9.3.9 '@nestjs/testing': 9.3.9 '@peertube/http-signature': 1.7.0 '@sinonjs/fake-timers': 10.0.2 '@swc/cli': 0.1.62 - '@swc/core': 1.3.36 + '@swc/core': 1.3.38 '@swc/core-android-arm64': ^1.3.11 - '@swc/core-darwin-arm64': ^1.3.36 - '@swc/core-darwin-x64': ^1.3.36 - '@swc/core-linux-arm-gnueabihf': ^1.3.36 - '@swc/core-linux-arm64-gnu': ^1.3.36 - '@swc/core-linux-arm64-musl': ^1.3.36 - '@swc/core-linux-x64-gnu': ^1.3.36 - '@swc/core-linux-x64-musl': ^1.3.36 - '@swc/core-win32-arm64-msvc': ^1.3.36 - '@swc/core-win32-ia32-msvc': ^1.3.36 - '@swc/core-win32-x64-msvc': ^1.3.36 + '@swc/core-darwin-arm64': ^1.3.38 + '@swc/core-darwin-x64': ^1.3.38 + '@swc/core-linux-arm-gnueabihf': ^1.3.38 + '@swc/core-linux-arm64-gnu': ^1.3.38 + '@swc/core-linux-arm64-musl': ^1.3.38 + '@swc/core-linux-x64-gnu': ^1.3.38 + '@swc/core-linux-x64-musl': ^1.3.38 + '@swc/core-win32-arm64-msvc': ^1.3.38 + '@swc/core-win32-ia32-msvc': ^1.3.38 + '@swc/core-win32-x64-msvc': ^1.3.38 '@swc/jest': 0.2.24 '@tensorflow/tfjs': 4.2.0 '@tensorflow/tfjs-node': 4.2.0 @@ -97,7 +97,7 @@ importers: '@types/jsonld': 1.5.8 '@types/jsrsasign': 10.5.5 '@types/mime-types': 2.1.1 - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/node-fetch': 3.0.3 '@types/nodemailer': 6.4.7 '@types/oauth': 0.9.1 @@ -109,7 +109,7 @@ importers: '@types/ratelimiter': 3.4.4 '@types/redis': 4.0.11 '@types/rename': 1.0.4 - '@types/sanitize-html': 2.8.0 + '@types/sanitize-html': 2.8.1 '@types/semver': 7.3.13 '@types/sharp': 0.31.1 '@types/sinonjs__fake-timers': 8.1.2 @@ -121,8 +121,8 @@ importers: '@types/web-push': 3.3.2 '@types/websocket': 1.0.5 '@types/ws': 8.5.4 - '@typescript-eslint/eslint-plugin': 5.52.0 - '@typescript-eslint/parser': 5.53.0 + '@typescript-eslint/eslint-plugin': 5.54.1 + '@typescript-eslint/parser': 5.54.1 accepts: 1.3.8 ajv: 8.12.0 archiver: 5.3.1 @@ -146,19 +146,19 @@ importers: eslint: 8.35.0 eslint-plugin-import: 2.27.5 execa: 6.1.0 - fastify: 4.13.0 + fastify: 4.14.1 feed: 4.2.2 file-type: 18.2.1 fluent-ffmpeg: 2.1.2 form-data: 4.0.0 - got: 12.5.3 + got: 12.6.0 happy-dom: 8.9.0 hpagent: 1.2.0 ioredis: 4.28.5 ip-cidr: 3.1.0 is-svg: 4.3.2 - jest: 29.4.3 - jest-mock: 29.4.3 + jest: 29.5.0 + jest-mock: 29.5.0 js-yaml: 4.1.0 jsdom: 21.1.0 json5: 2.2.3 @@ -176,7 +176,7 @@ importers: os-utils: 0.0.14 otpauth: ^9.0.2 parse5: 7.1.2 - pg: 8.9.0 + pg: 8.10.0 private-ip: 3.0.0 probe-image-size: 7.2.3 promise-limit: 2.7.0 @@ -202,10 +202,10 @@ importers: strict-event-emitter-types: 2.0.0 stringz: 2.1.0 summaly: github:misskey-dev/summaly - systeminformation: 5.17.10 + systeminformation: 5.17.12 tinycolor2: 1.6.0 tmp: 0.2.1 - tsc-alias: 1.8.2 + tsc-alias: 1.8.3 tsconfig-paths: 4.1.2 twemoji-parser: 14.0.0 typeorm: 0.3.11 @@ -219,15 +219,15 @@ importers: ws: 8.12.1 xev: 3.0.2 dependencies: - '@bull-board/api': 4.12.1 - '@bull-board/fastify': 4.12.1 - '@bull-board/ui': 4.12.1 + '@bull-board/api': 5.0.0 + '@bull-board/fastify': 5.0.0 + '@bull-board/ui': 5.0.0 '@discordapp/twemoji': 14.0.2 '@fastify/accepts': 4.1.0 '@fastify/cookie': 8.3.0 '@fastify/cors': 8.2.0 '@fastify/http-proxy': 8.4.0 - '@fastify/multipart': 7.4.1 + '@fastify/multipart': 7.4.2 '@fastify/static': 6.9.0 '@fastify/view': 7.4.1 '@nestjs/common': 9.3.9_mnr6j2del53muneqly5h4y27ai @@ -235,8 +235,8 @@ importers: '@nestjs/testing': 9.3.9_77foi4w27ghy47yutmnzv7krjy '@peertube/http-signature': 1.7.0 '@sinonjs/fake-timers': 10.0.2 - '@swc/cli': 0.1.62_wyduggqpvtv2oy5nc7cgtozgpy - '@swc/core': 1.3.36 + '@swc/cli': 0.1.62_2u6773zfichz7q5gjuzddfwg7q + '@swc/core': 1.3.38 accepts: 1.3.8 ajv: 8.12.0 archiver: 5.3.1 @@ -256,12 +256,12 @@ importers: date-fns: 2.29.3 deep-email-validator: 0.1.21 escape-regexp: 0.0.1 - fastify: 4.13.0 + fastify: 4.14.1 feed: 4.2.2 file-type: 18.2.1 fluent-ffmpeg: 2.1.2 form-data: 4.0.0 - got: 12.5.3 + got: 12.6.0 happy-dom: 8.9.0 hpagent: 1.2.0 ioredis: 4.28.5 @@ -284,7 +284,7 @@ importers: os-utils: 0.0.14 otpauth: 9.0.2 parse5: 7.1.2 - pg: 8.9.0 + pg: 8.10.0 private-ip: 3.0.0 probe-image-size: 7.2.3 promise-limit: 2.7.0 @@ -309,14 +309,14 @@ importers: sharp-read-bmp: github.com/misskey-dev/sharp-read-bmp/02d9dc189fa7df0c4bea09330be26741772dac01 strict-event-emitter-types: 2.0.0 stringz: 2.1.0 - summaly: github.com/misskey-dev/summaly/51f3870e1ff5e0b22102e804112b10cb72f3c494 - systeminformation: 5.17.10 + summaly: github.com/misskey-dev/summaly/1bab7afee616429b8bbf7a7cbcbb8ebcef66d992 + systeminformation: 5.17.12 tinycolor2: 1.6.0 tmp: 0.2.1 - tsc-alias: 1.8.2 + tsc-alias: 1.8.3 tsconfig-paths: 4.1.2 twemoji-parser: 14.0.0 - typeorm: 0.3.11_ioredis@4.28.5+pg@8.9.0 + typeorm: 0.3.11_ioredis@4.28.5+pg@8.10.0 typescript: 4.9.5 ulid: 2.3.0 unzipper: 0.10.11 @@ -328,21 +328,21 @@ importers: xev: 3.0.2 optionalDependencies: '@swc/core-android-arm64': 1.3.11 - '@swc/core-darwin-arm64': 1.3.36 - '@swc/core-darwin-x64': 1.3.36 - '@swc/core-linux-arm-gnueabihf': 1.3.36 - '@swc/core-linux-arm64-gnu': 1.3.36 - '@swc/core-linux-arm64-musl': 1.3.36 - '@swc/core-linux-x64-gnu': 1.3.36 - '@swc/core-linux-x64-musl': 1.3.36 - '@swc/core-win32-arm64-msvc': 1.3.36 - '@swc/core-win32-ia32-msvc': 1.3.36 - '@swc/core-win32-x64-msvc': 1.3.36 + '@swc/core-darwin-arm64': 1.3.38 + '@swc/core-darwin-x64': 1.3.38 + '@swc/core-linux-arm-gnueabihf': 1.3.38 + '@swc/core-linux-arm64-gnu': 1.3.38 + '@swc/core-linux-arm64-musl': 1.3.38 + '@swc/core-linux-x64-gnu': 1.3.38 + '@swc/core-linux-x64-musl': 1.3.38 + '@swc/core-win32-arm64-msvc': 1.3.38 + '@swc/core-win32-ia32-msvc': 1.3.38 + '@swc/core-win32-x64-msvc': 1.3.38 '@tensorflow/tfjs': 4.2.0_seedrandom@3.0.5 '@tensorflow/tfjs-node': 4.2.0_seedrandom@3.0.5 devDependencies: - '@jest/globals': 29.4.3 - '@swc/jest': 0.2.24_@swc+core@1.3.36 + '@jest/globals': 29.5.0 + '@swc/jest': 0.2.24_@swc+core@1.3.38 '@types/accepts': 1.3.5 '@types/archiver': 5.3.1 '@types/bcryptjs': 2.4.2 @@ -359,7 +359,7 @@ importers: '@types/jsonld': 1.5.8 '@types/jsrsasign': 10.5.5 '@types/mime-types': 2.1.1 - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/node-fetch': 3.0.3 '@types/nodemailer': 6.4.7 '@types/oauth': 0.9.1 @@ -371,7 +371,7 @@ importers: '@types/ratelimiter': 3.4.4 '@types/redis': 4.0.11 '@types/rename': 1.0.4 - '@types/sanitize-html': 2.8.0 + '@types/sanitize-html': 2.8.1 '@types/semver': 7.3.13 '@types/sharp': 0.31.1 '@types/sinonjs__fake-timers': 8.1.2 @@ -383,14 +383,14 @@ importers: '@types/web-push': 3.3.2 '@types/websocket': 1.0.5 '@types/ws': 8.5.4 - '@typescript-eslint/eslint-plugin': 5.52.0_cjo54hduev4bqhpjw5znwiokqu - '@typescript-eslint/parser': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu + '@typescript-eslint/eslint-plugin': 5.54.1_mlk7dnz565t663n4razh6a6v6i + '@typescript-eslint/parser': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu cross-env: 7.0.3 eslint: 8.35.0 - eslint-plugin-import: 2.27.5_nhka4er4oejxhxq3ecgtwxvdji + eslint-plugin-import: 2.27.5_uyiasnnzcqrxqkfvjklwnmwcha execa: 6.1.0 - jest: 29.4.3_@types+node@18.14.1 - jest-mock: 29.4.3 + jest: 29.5.0_@types+node@18.15.0 + jest-mock: 29.5.0 packages/frontend: specifiers: @@ -398,24 +398,26 @@ importers: '@rollup/plugin-alias': 4.0.3 '@rollup/plugin-json': 6.0.0 '@rollup/pluginutils': 5.0.2 - '@syuilo/aiscript': 0.12.4 - '@tabler/icons-webfont': 2.2.0 + '@syuilo/aiscript': 0.13.1 + '@tabler/icons-webfont': 2.10.0 + '@testing-library/vue': ^6.6.1 '@types/escape-regexp': 0.0.1 '@types/gulp': 4.0.10 '@types/gulp-rename': 2.0.1 '@types/matter-js': 0.18.2 - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/punycode': 2.1.0 - '@types/sanitize-html': 2.8.0 + '@types/sanitize-html': 2.8.1 '@types/seedrandom': 3.0.5 '@types/throttle-debounce': 5.0.0 '@types/tinycolor2': 1.4.3 '@types/uuid': 9.0.1 '@types/websocket': 1.0.5 '@types/ws': 8.5.4 - '@typescript-eslint/eslint-plugin': 5.53.0 - '@typescript-eslint/parser': 5.53.0 + '@typescript-eslint/eslint-plugin': 5.54.1 + '@typescript-eslint/parser': 5.54.1 '@vitejs/plugin-vue': 4.0.0 + '@vitest/coverage-c8': ^0.29.2 '@vue/compiler-sfc': 3.2.47 '@vue/runtime-core': 3.2.47 autobind-decorator: 2.4.0 @@ -440,6 +442,7 @@ importers: eslint-plugin-vue: 9.9.0 eventemitter3: 5.0.0 gsap: 3.11.4 + happy-dom: 8.9.0 idb-keyval: 6.2.0 insert-text-at-cursor: 0.3.0 is-file-animated: 1.0.2 @@ -452,25 +455,28 @@ importers: punycode: 2.3.0 querystring: 0.2.1 rndstr: 1.0.0 - rollup: 3.17.3 + rollup: 3.19.0 s-age: 1.1.2 sanitize-html: 2.10.0 sass: 1.58.3 seedrandom: 3.0.5 - start-server-and-test: 1.15.4 + start-server-and-test: 2.0.0 strict-event-emitter-types: 2.0.0 + summaly: github:misskey-dev/summaly syuilo-password-strength: 0.0.1 textarea-caret: 3.1.0 - three: 0.150.0 + three: 0.150.1 throttle-debounce: 5.0.0 tinycolor2: 1.6.0 - tsc-alias: 1.8.2 + tsc-alias: 1.8.3 tsconfig-paths: 4.1.2 twemoji-parser: 14.0.0 typescript: 4.9.5 uuid: 9.0.0 vanilla-tilt: 1.8.0 vite: 4.1.4 + vitest: ^0.29.2 + vitest-fetch-mock: ^0.2.2 vue: 3.2.47 vue-eslint-parser: 9.1.0 vue-plyr: 7.0.0 @@ -479,11 +485,11 @@ importers: vuedraggable: next dependencies: '@discordapp/twemoji': 14.0.2 - '@rollup/plugin-alias': 4.0.3_rollup@3.17.3 - '@rollup/plugin-json': 6.0.0_rollup@3.17.3 - '@rollup/pluginutils': 5.0.2_rollup@3.17.3 - '@syuilo/aiscript': 0.12.4 - '@tabler/icons-webfont': 2.2.0 + '@rollup/plugin-alias': 4.0.3_rollup@3.19.0 + '@rollup/plugin-json': 6.0.0_rollup@3.19.0 + '@rollup/pluginutils': 5.0.2_rollup@3.19.0 + '@syuilo/aiscript': 0.13.1 + '@tabler/icons-webfont': 2.10.0 '@vitejs/plugin-vue': 4.0.0_vite@4.1.4+vue@3.2.47 '@vue/compiler-sfc': 3.2.47 autobind-decorator: 2.4.0 @@ -515,7 +521,7 @@ importers: punycode: 2.3.0 querystring: 0.2.1 rndstr: 1.0.0 - rollup: 3.17.3 + rollup: 3.19.0 s-age: 1.1.2 sanitize-html: 2.10.0 sass: 1.58.3 @@ -523,43 +529,49 @@ importers: strict-event-emitter-types: 2.0.0 syuilo-password-strength: 0.0.1 textarea-caret: 3.1.0 - three: 0.150.0 + three: 0.150.1 throttle-debounce: 5.0.0 tinycolor2: 1.6.0 - tsc-alias: 1.8.2 + tsc-alias: 1.8.3 tsconfig-paths: 4.1.2 twemoji-parser: 14.0.0 typescript: 4.9.5 uuid: 9.0.0 vanilla-tilt: 1.8.0 - vite: 4.1.4_435aevtanapkguv7m72cl6trbi + vite: 4.1.4_6e4omgvd5jf4hig7wpb5tmdc3q vue: 3.2.47 vue-plyr: 7.0.0 vue-prism-editor: 2.0.0-alpha.2_vue@3.2.47 vuedraggable: 4.1.0_vue@3.2.47 devDependencies: + '@testing-library/vue': 6.6.1_a2ihsjreowava2sm4iorpgwkom '@types/escape-regexp': 0.0.1 '@types/gulp': 4.0.10 '@types/gulp-rename': 2.0.1 '@types/matter-js': 0.18.2 - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/punycode': 2.1.0 - '@types/sanitize-html': 2.8.0 + '@types/sanitize-html': 2.8.1 '@types/seedrandom': 3.0.5 '@types/throttle-debounce': 5.0.0 '@types/tinycolor2': 1.4.3 '@types/uuid': 9.0.1 '@types/websocket': 1.0.5 '@types/ws': 8.5.4 - '@typescript-eslint/eslint-plugin': 5.53.0_cjo54hduev4bqhpjw5znwiokqu - '@typescript-eslint/parser': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu + '@typescript-eslint/eslint-plugin': 5.54.1_mlk7dnz565t663n4razh6a6v6i + '@typescript-eslint/parser': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu + '@vitest/coverage-c8': 0.29.2_vitest@0.29.2 '@vue/runtime-core': 3.2.47 cross-env: 7.0.3 cypress: 12.7.0 eslint: 8.35.0 - eslint-plugin-import: 2.27.5_nhka4er4oejxhxq3ecgtwxvdji + eslint-plugin-import: 2.27.5_uyiasnnzcqrxqkfvjklwnmwcha eslint-plugin-vue: 9.9.0_eslint@8.35.0 - start-server-and-test: 1.15.4 + happy-dom: 8.9.0 + start-server-and-test: 2.0.0 + summaly: github.com/misskey-dev/summaly/1bab7afee616429b8bbf7a7cbcbb8ebcef66d992 + vitest: 0.29.2_zcjcryjt4bqcdu7ggonulipgea + vitest-fetch-mock: 0.2.2_vitest@0.29.2 vue-eslint-parser: 9.1.0_eslint@8.35.0 vue-tsc: 1.2.0_typescript@4.9.5 @@ -750,6 +762,7 @@ packages: /@babel/parser/7.20.7: resolution: {integrity: sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==} engines: {node: '>=6.0.0'} + hasBin: true dependencies: '@babel/types': 7.20.7 @@ -887,7 +900,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.11 - dev: false /@babel/template/7.20.7: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} @@ -928,26 +940,26 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - /@bull-board/api/4.12.1: - resolution: {integrity: sha512-cJk7LhphNZHwbN4yON5rYiDgob3D7cwFGrxf6LlnR1w/a6zyap1x/o74MfV/0T6NpUpvUSf7ccbj3knU6DNYVw==} + /@bull-board/api/5.0.0: + resolution: {integrity: sha512-nOB8TUSeX8h2wgMxmPZwy0p3XqRX/DotR71M5FT5OS/bZCZIKRpEc36TIeKPimZli6voFyX+rsEqVhGenpIKUQ==} dependencies: redis-info: 3.1.0 dev: false - /@bull-board/fastify/4.12.1: - resolution: {integrity: sha512-MAecvp+xTePdEHm4uCooojAlj2isdznfC9XuCjjGHHNCczjm/Wx14cDhKLKwkOFp5ZLhqgtOEulT5YVfVCu+EQ==} + /@bull-board/fastify/5.0.0: + resolution: {integrity: sha512-g7eLQhap9HiRjWv3NkgbVrdiG8RDUwyA3qDW2jES1dRcpWPBD9/3rxESwzuIoxa2h0OJ34VLY9hsgpu0vRyhZg==} dependencies: - '@bull-board/api': 4.12.1 - '@bull-board/ui': 4.12.1 + '@bull-board/api': 5.0.0 + '@bull-board/ui': 5.0.0 '@fastify/static': 6.9.0 '@fastify/view': 7.4.1 ejs: 3.1.8 dev: false - /@bull-board/ui/4.12.1: - resolution: {integrity: sha512-nNTjxitLEre+UdZjwMHwlKKGxpg3OV/Sf7dNOlhJYoRjO7aYPb63M+BfGVrYgupk3V73Q0qvl1emtsnF/qz/Wg==} + /@bull-board/ui/5.0.0: + resolution: {integrity: sha512-WznLX8dGthUWimZDqN5Zft3Axp6SStlI244BhiQYXB7xXZRMgaihNZxh7vgpwg0Osmw/gPFvAI+NFVGc3tvj9g==} dependencies: - '@bull-board/api': 4.12.1 + '@bull-board/api': 5.0.0 dev: false /@canvas/image-data/1.0.0: @@ -1112,7 +1124,6 @@ packages: cpu: [arm] os: [android] requiresBuild: true - dev: false optional: true /@esbuild/android-arm64/0.16.17: @@ -1121,7 +1132,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: false optional: true /@esbuild/android-x64/0.16.17: @@ -1130,7 +1140,6 @@ packages: cpu: [x64] os: [android] requiresBuild: true - dev: false optional: true /@esbuild/darwin-arm64/0.16.17: @@ -1139,7 +1148,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: false optional: true /@esbuild/darwin-x64/0.16.17: @@ -1148,7 +1156,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: false optional: true /@esbuild/freebsd-arm64/0.16.17: @@ -1157,7 +1164,6 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true - dev: false optional: true /@esbuild/freebsd-x64/0.16.17: @@ -1166,7 +1172,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: false optional: true /@esbuild/linux-arm/0.16.17: @@ -1175,7 +1180,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: false optional: true /@esbuild/linux-arm64/0.16.17: @@ -1184,7 +1188,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false optional: true /@esbuild/linux-ia32/0.16.17: @@ -1193,7 +1196,6 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true - dev: false optional: true /@esbuild/linux-loong64/0.16.17: @@ -1202,7 +1204,6 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true - dev: false optional: true /@esbuild/linux-mips64el/0.16.17: @@ -1211,7 +1212,6 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true - dev: false optional: true /@esbuild/linux-ppc64/0.16.17: @@ -1220,7 +1220,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: false optional: true /@esbuild/linux-riscv64/0.16.17: @@ -1229,7 +1228,6 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: false optional: true /@esbuild/linux-s390x/0.16.17: @@ -1238,7 +1236,6 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: false optional: true /@esbuild/linux-x64/0.16.17: @@ -1247,7 +1244,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false optional: true /@esbuild/netbsd-x64/0.16.17: @@ -1256,7 +1252,6 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true - dev: false optional: true /@esbuild/openbsd-x64/0.16.17: @@ -1265,7 +1260,6 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: false optional: true /@esbuild/sunos-x64/0.16.17: @@ -1274,7 +1268,6 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true - dev: false optional: true /@esbuild/win32-arm64/0.16.17: @@ -1283,7 +1276,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: false optional: true /@esbuild/win32-ia32/0.16.17: @@ -1292,7 +1284,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: false optional: true /@esbuild/win32-x64/0.16.17: @@ -1301,7 +1292,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: false optional: true /@eslint/eslintrc/1.4.1: @@ -1408,8 +1398,8 @@ packages: - utf-8-validate dev: false - /@fastify/multipart/7.4.1: - resolution: {integrity: sha512-PgBJIg/1krx1nWIXGZSUFQMPHXeFv559TmpaswzmNkK6V/yn1xi4nkATqBetv7s5XbXhxzc75uaJgXWdUefxvQ==} + /@fastify/multipart/7.4.2: + resolution: {integrity: sha512-jrPoQ8I2Mpd92vVR/XE1DKBa+h7EiXaLL5TvHIUTtHiF7/0FChjP7spBQ9ROItpNrNKdKGB8uSJHmPIjRK/24A==} dependencies: '@fastify/busboy': 1.1.0 '@fastify/deepmerge': 1.3.0 @@ -1515,20 +1505,20 @@ packages: engines: {node: '>=8'} dev: true - /@jest/console/29.4.3: - resolution: {integrity: sha512-W/o/34+wQuXlgqlPYTansOSiBnuxrTv61dEVkA6HNmpcgHLUjfaUbdqt6oVvOzaawwo9IdW9QOtMgQ1ScSZC4A==} + /@jest/console/29.5.0: + resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 - '@types/node': 18.14.1 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 chalk: 4.1.2 - jest-message-util: 29.4.3 - jest-util: 29.4.3 + jest-message-util: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 dev: true - /@jest/core/29.4.3: - resolution: {integrity: sha512-56QvBq60fS4SPZCuM7T+7scNrkGIe7Mr6PVIXUpu48ouvRaWOFqRPV91eifvFM0ay2HmfswXiGf97NGUN5KofQ==} + /@jest/core/29.5.0: + resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -1536,32 +1526,32 @@ packages: node-notifier: optional: true dependencies: - '@jest/console': 29.4.3 - '@jest/reporters': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 18.14.1 + '@jest/console': 29.5.0 + '@jest/reporters': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.1 exit: 0.1.2 graceful-fs: 4.2.10 - jest-changed-files: 29.4.3 - jest-config: 29.4.3_@types+node@18.14.1 - jest-haste-map: 29.4.3 - jest-message-util: 29.4.3 + jest-changed-files: 29.5.0 + jest-config: 29.5.0_@types+node@18.15.0 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-resolve-dependencies: 29.4.3 - jest-runner: 29.4.3 - jest-runtime: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 - jest-watcher: 29.4.3 + jest-resolve: 29.5.0 + jest-resolve-dependencies: 29.5.0 + jest-runner: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + jest-watcher: 29.5.0 micromatch: 4.0.5 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: @@ -1576,66 +1566,66 @@ packages: '@jest/types': 27.5.1 dev: true - /@jest/environment/29.4.3: - resolution: {integrity: sha512-dq5S6408IxIa+lr54zeqce+QgI+CJT4nmmA+1yzFgtcsGK8c/EyiUb9XQOgz3BMKrRDfKseeOaxj2eO8LlD3lA==} + /@jest/environment/29.5.0: + resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/fake-timers': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 18.14.1 - jest-mock: 29.4.3 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 + jest-mock: 29.5.0 dev: true /@jest/expect-utils/29.3.1: resolution: {integrity: sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.4.2 + jest-get-type: 29.4.3 dev: true - /@jest/expect-utils/29.4.3: - resolution: {integrity: sha512-/6JWbkxHOP8EoS8jeeTd9dTfc9Uawi+43oLKHfp6zzux3U2hqOOVnV3ai4RpDYHOccL6g+5nrxpoc8DmJxtXVQ==} + /@jest/expect-utils/29.5.0: + resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.4.3 dev: true - /@jest/expect/29.4.3: - resolution: {integrity: sha512-iktRU/YsxEtumI9zsPctYUk7ptpC+AVLLk1Ax3AsA4g1C+8OOnKDkIQBDHtD5hA/+VtgMd5AWI5gNlcAlt2vxQ==} + /@jest/expect/29.5.0: + resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.4.3 - jest-snapshot: 29.4.3 + expect: 29.5.0 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color dev: true - /@jest/fake-timers/29.4.3: - resolution: {integrity: sha512-4Hote2MGcCTWSD2gwl0dwbCpBRHhE6olYEuTj8FMowdg3oQWNKr2YuxenPQYZ7+PfqPY1k98wKDU4Z+Hvd4Tiw==} + /@jest/fake-timers/29.5.0: + resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@sinonjs/fake-timers': 10.0.2 - '@types/node': 18.14.1 - jest-message-util: 29.4.3 - jest-mock: 29.4.3 - jest-util: 29.4.3 + '@types/node': 18.15.0 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 + jest-util: 29.5.0 dev: true - /@jest/globals/29.4.3: - resolution: {integrity: sha512-8BQ/5EzfOLG7AaMcDh7yFCbfRLtsc+09E1RQmRBI4D6QQk4m6NSK/MXo+3bJrBN0yU8A2/VIcqhvsOLFmziioA==} + /@jest/globals/29.5.0: + resolution: {integrity: sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/expect': 29.4.3 - '@jest/types': 29.4.3 - jest-mock: 29.4.3 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/types': 29.5.0 + jest-mock: 29.5.0 transitivePeerDependencies: - supports-color dev: true - /@jest/reporters/29.4.3: - resolution: {integrity: sha512-sr2I7BmOjJhyqj9ANC6CTLsL4emMoka7HkQpcoMRlhCbQJjz2zsRzw0BDPiPyEFDXAbxKgGFYuQZiSJ1Y6YoTg==} + /@jest/reporters/29.5.0: + resolution: {integrity: sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -1644,12 +1634,12 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 - '@types/node': 18.14.1 + '@types/node': 18.15.0 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -1660,9 +1650,9 @@ packages: istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 - jest-message-util: 29.4.3 - jest-util: 29.4.3 - jest-worker: 29.4.3 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + jest-worker: 29.5.0 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 @@ -1694,41 +1684,41 @@ packages: graceful-fs: 4.2.10 dev: true - /@jest/test-result/29.4.3: - resolution: {integrity: sha512-Oi4u9NfBolMq9MASPwuWTlC5WvmNRwI4S8YrQg5R5Gi47DYlBe3sh7ILTqi/LGrK1XUE4XY9KZcQJTH1WJCLLA==} + /@jest/test-result/29.5.0: + resolution: {integrity: sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/types': 29.5.0 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.1 dev: true - /@jest/test-sequencer/29.4.3: - resolution: {integrity: sha512-yi/t2nES4GB4G0mjLc0RInCq/cNr9dNwJxcGg8sslajua5Kb4kmozAc+qPLzplhBgfw1vLItbjyHzUN92UXicw==} + /@jest/test-sequencer/29.5.0: + resolution: {integrity: sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.4.3 + '@jest/test-result': 29.5.0 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 + jest-haste-map: 29.5.0 slash: 3.0.0 dev: true - /@jest/transform/29.4.3: - resolution: {integrity: sha512-8u0+fBGWolDshsFgPQJESkDa72da/EVwvL+II0trN2DR66wMwiQ9/CihaGfHdlLGFzbBZwMykFtxuwFdZqlKwg==} + /@jest/transform/29.5.0: + resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.20.12 - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 + jest-haste-map: 29.5.0 jest-regex-util: 29.4.3 - jest-util: 29.4.3 + jest-util: 29.5.0 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 @@ -1743,19 +1733,19 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true - /@jest/types/29.4.3: - resolution: {integrity: sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA==} + /@jest/types/29.5.0: + resolution: {integrity: sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/yargs': 17.0.19 chalk: 4.1.2 dev: true @@ -2081,7 +2071,7 @@ packages: '@redis/client': 1.4.2 dev: true - /@rollup/plugin-alias/4.0.3_rollup@3.17.3: + /@rollup/plugin-alias/4.0.3_rollup@3.19.0: resolution: {integrity: sha512-ZuDWE1q4PQDhvm/zc5Prun8sBpLJy41DMptYrS6MhAy9s9kL/doN1613BWfEchGVfKxzliJ3BjbOPizXX38DbQ==} engines: {node: '>=14.0.0'} peerDependencies: @@ -2090,11 +2080,11 @@ packages: rollup: optional: true dependencies: - rollup: 3.17.3 + rollup: 3.19.0 slash: 4.0.0 dev: false - /@rollup/plugin-json/6.0.0_rollup@3.17.3: + /@rollup/plugin-json/6.0.0_rollup@3.19.0: resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==} engines: {node: '>=14.0.0'} peerDependencies: @@ -2103,11 +2093,11 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.2_rollup@3.17.3 - rollup: 3.17.3 + '@rollup/pluginutils': 5.0.2_rollup@3.19.0 + rollup: 3.19.0 dev: false - /@rollup/pluginutils/5.0.2_rollup@3.17.3: + /@rollup/pluginutils/5.0.2_rollup@3.19.0: resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -2119,7 +2109,7 @@ packages: '@types/estree': 1.0.0 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 3.17.3 + rollup: 3.19.0 dev: false /@sideway/address/4.1.4: @@ -2148,7 +2138,6 @@ packages: /@sindresorhus/is/5.3.0: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} - dev: false /@sinonjs/commons/2.0.0: resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} @@ -2164,7 +2153,7 @@ packages: resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} dev: false - /@swc/cli/0.1.62_wyduggqpvtv2oy5nc7cgtozgpy: + /@swc/cli/0.1.62_2u6773zfichz7q5gjuzddfwg7q: resolution: {integrity: sha512-kOFLjKY3XH1DWLfXL1/B5MizeNorHR8wHKEi92S/Zi9Md/AK17KSqR8MgyRJ6C1fhKHvbBCl8wboyKAFXStkYw==} engines: {node: '>= 12.13'} hasBin: true @@ -2176,7 +2165,7 @@ packages: optional: true dependencies: '@mole-inc/bin-wrapper': 8.0.1 - '@swc/core': 1.3.36 + '@swc/core': 1.3.38 chokidar: 3.5.3 commander: 7.2.0 fast-glob: 3.2.12 @@ -2196,110 +2185,110 @@ packages: dev: false optional: true - /@swc/core-darwin-arm64/1.3.36: - resolution: {integrity: sha512-lsP+C8p9cC/Vd9uAbtxpEnM8GoJI/MMnVuXak7OlxOtDH9/oTwmAcAQTfNGNaH19d2FAIRwf+5RbXCPnxa2Zjw==} + /@swc/core-darwin-arm64/1.3.38: + resolution: {integrity: sha512-4ZTJJ/cR0EsXW5UxFCifZoGfzQ07a8s4ayt1nLvLQ5QoB1GTAf9zsACpvWG8e7cmCR0L76R5xt8uJuyr+noIXA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@swc/core-darwin-x64/1.3.36: - resolution: {integrity: sha512-jaLXsozWN5xachl9fPxDMi5nbWq1rRxPAt6ISeiYB6RJk0MQKH1634pOweBBem2pUDDzwDFXFw6f22LTm/cFvA==} + /@swc/core-darwin-x64/1.3.38: + resolution: {integrity: sha512-Kim727rNo4Dl8kk0CR8aJQe4zFFtsT1TZGlNrNMUgN1WC3CRX7dLZ6ZJi/VVcTG1cbHp5Fp3mUzwHsMxEh87Mg==} engines: {node: '>=10'} cpu: [x64] os: [darwin] requiresBuild: true optional: true - /@swc/core-linux-arm-gnueabihf/1.3.36: - resolution: {integrity: sha512-vcBdTHjoEpvJDbFlgto+S6VwAHzLA9GyCiuNcTU2v4KNQlFzhbO4A4PMfMCb/Z0RLJEr16tirfHdWIxjU3h8nw==} + /@swc/core-linux-arm-gnueabihf/1.3.38: + resolution: {integrity: sha512-yaRdnPNU2enlJDRcIMvYVSyodY+Amhf5QuXdUbAj6rkDD6wUs/s9C6yPYrFDmoTltrG+nBv72mUZj+R46wVfSw==} engines: {node: '>=10'} cpu: [arm] os: [linux] requiresBuild: true optional: true - /@swc/core-linux-arm64-gnu/1.3.36: - resolution: {integrity: sha512-o7f5OsvwWppJo+qIZmrGO5+XC6DPt6noecSbRHjF6o1YAcR13ETPC14k1eC9H1YbQwpyCFNVAFXyNcUbCeQyrQ==} + /@swc/core-linux-arm64-gnu/1.3.38: + resolution: {integrity: sha512-iNY1HqKo/wBSu3QOGBUlZaLdBP/EHcwNjBAqIzpb8J64q2jEN02RizqVW0mDxyXktJ3lxr3g7VW9uqklMeXbjQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@swc/core-linux-arm64-musl/1.3.36: - resolution: {integrity: sha512-FSHPngMi3c0fuGt9yY2Ubn5UcELi3EiPLJxBSC3X8TF9atI/WHZzK9PE9Gtn0C/LyRh4CoyOugDtSOPzGYmLQg==} + /@swc/core-linux-arm64-musl/1.3.38: + resolution: {integrity: sha512-LJCFgLZoPRkPCPmux+Q5ctgXRp6AsWhvWuY61bh5bIPBDlaG9pZk94DeHyvtiwT0syhTtXb2LieBOx6NqN3zeA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@swc/core-linux-x64-gnu/1.3.36: - resolution: {integrity: sha512-PHSsH2rek5pr3e0K09VgWAbrWK2vJhaI7MW9TPoTjyACYjcs3WwjcjQ30MghXUs2Dc/bXjWAOi9KFTjq/uCyFg==} + /@swc/core-linux-x64-gnu/1.3.38: + resolution: {integrity: sha512-hRQGRIWHmv2PvKQM/mMV45mVXckM2+xLB8TYLLgUG66mmtyGTUJPyxjnJkbI86WNGqo18k+lAuMG2mn6QmzYwQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@swc/core-linux-x64-musl/1.3.36: - resolution: {integrity: sha512-4LfMYQHzozHCKkIcmQy83b+4SpI+mOp6sYNbXqSRz5dYvTVjegKZXe596P1U/87cK2cgR4uYvkgkgBXquaWvwQ==} + /@swc/core-linux-x64-musl/1.3.38: + resolution: {integrity: sha512-PTYSqtsIfPHLKDDNbueI5e0sc130vyHRiFOeeC6qqzA2FAiVvIxuvXHLr0soPvKAR1WyhtYmFB9QarcctemL2w==} engines: {node: '>=10'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@swc/core-win32-arm64-msvc/1.3.36: - resolution: {integrity: sha512-7y3dDcun79TAjCyk3Iv0eOMw1X/KNQbkVyKOGqnEgq9g22F8F1FoUGKHNTzUqVdzpHeJSsHgW5PlkEkl3c/d9w==} + /@swc/core-win32-arm64-msvc/1.3.38: + resolution: {integrity: sha512-9lHfs5TPNs+QdkyZFhZledSmzBEbqml/J1rqPSb9Fy8zB6QlspixE6OLZ3nTlUOdoGWkcTTdrOn77Sd7YGf1AA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] requiresBuild: true optional: true - /@swc/core-win32-ia32-msvc/1.3.36: - resolution: {integrity: sha512-zK0VR3B4LX5hzQ+7eD+K+FkxJlJg5Lo36BeahMzQ+/i0IURpnuyFlW88sdkFkMsc2swdU6bpvxLZeIRQ3W4OUg==} + /@swc/core-win32-ia32-msvc/1.3.38: + resolution: {integrity: sha512-SbL6pfA2lqvDKnwTHwOfKWvfHAdcbAwJS4dBkFidr7BiPTgI5Uk8wAPcRb8mBECpmIa9yFo+N0cAFRvMnf+cNw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] requiresBuild: true optional: true - /@swc/core-win32-x64-msvc/1.3.36: - resolution: {integrity: sha512-2bIjr9DhAckGiXZEvj6z2z7ECPcTimG+wD0VuQTvr+wkx46uAJKl5Kq+Zk+dd15ErL7JGUtCet1T7bf1k4FwvQ==} + /@swc/core-win32-x64-msvc/1.3.38: + resolution: {integrity: sha512-UFveLrL6eGvViOD8OVqUQa6QoQwdqwRvLtL5elF304OT8eCPZa8BhuXnWk25X8UcOyns8gFcb8Fhp3oaLi/Rlw==} engines: {node: '>=10'} cpu: [x64] os: [win32] requiresBuild: true optional: true - /@swc/core/1.3.36: - resolution: {integrity: sha512-Ogrd9uRNIj7nHjXxG66UlKBIcXESUenJ7OD6K2a8p82qlg6ne7Ne5Goiipm/heHYhSfVmjcnRWL9ZJ4gv+YCPA==} + /@swc/core/1.3.38: + resolution: {integrity: sha512-AiEVehRFws//AiiLx9DPDp1WDXt+yAoGD1kMYewhoF6QLdTz8AtYu6i8j/yAxk26L8xnegy0CDwcNnub9qenyQ==} engines: {node: '>=10'} requiresBuild: true optionalDependencies: - '@swc/core-darwin-arm64': 1.3.36 - '@swc/core-darwin-x64': 1.3.36 - '@swc/core-linux-arm-gnueabihf': 1.3.36 - '@swc/core-linux-arm64-gnu': 1.3.36 - '@swc/core-linux-arm64-musl': 1.3.36 - '@swc/core-linux-x64-gnu': 1.3.36 - '@swc/core-linux-x64-musl': 1.3.36 - '@swc/core-win32-arm64-msvc': 1.3.36 - '@swc/core-win32-ia32-msvc': 1.3.36 - '@swc/core-win32-x64-msvc': 1.3.36 + '@swc/core-darwin-arm64': 1.3.38 + '@swc/core-darwin-x64': 1.3.38 + '@swc/core-linux-arm-gnueabihf': 1.3.38 + '@swc/core-linux-arm64-gnu': 1.3.38 + '@swc/core-linux-arm64-musl': 1.3.38 + '@swc/core-linux-x64-gnu': 1.3.38 + '@swc/core-linux-x64-musl': 1.3.38 + '@swc/core-win32-arm64-msvc': 1.3.38 + '@swc/core-win32-ia32-msvc': 1.3.38 + '@swc/core-win32-x64-msvc': 1.3.38 - /@swc/jest/0.2.24_@swc+core@1.3.36: + /@swc/jest/0.2.24_@swc+core@1.3.38: resolution: {integrity: sha512-fwgxQbM1wXzyKzl1+IW0aGrRvAA8k0Y3NxFhKigbPjOJ4mCKnWEcNX9HQS3gshflcxq8YKhadabGUVfdwjCr6Q==} engines: {npm: '>= 7.0.0'} peerDependencies: '@swc/core': '*' dependencies: '@jest/create-cache-key-function': 27.5.1 - '@swc/core': 1.3.36 + '@swc/core': 1.3.38 jsonc-parser: 3.2.0 dev: true @@ -2309,8 +2298,8 @@ packages: dev: false optional: true - /@syuilo/aiscript/0.12.4: - resolution: {integrity: sha512-fIWEAHsnw9vMWTcjmSM2rCsadhb+BLirLDLW6lnRw3PFC8LV76Np8Ih8ssfAb00udh6oiGBSj+WiBdkMagbjmQ==} + /@syuilo/aiscript/0.13.1: + resolution: {integrity: sha512-WJduqlsm7pq8r1oYbPSAf5cqR6Pve6ipGoPUUSgti51dMytMnxE2dn7d7i8759RF3yftyoclZbL/DC7IjPsTng==} dependencies: autobind-decorator: 2.4.0 seedrandom: 3.0.5 @@ -2330,16 +2319,15 @@ packages: engines: {node: '>=14.16'} dependencies: defer-to-connect: 2.0.1 - dev: false - /@tabler/icons-webfont/2.2.0: - resolution: {integrity: sha512-rAok1gpbi3XzsgyXGAg1jNueXm1L5YGR2ab0RWMR8T8W8x6Kl2IlCOPZ5mHTJoac+2BA3s9dAPRsMM47nfPMYw==} + /@tabler/icons-webfont/2.10.0: + resolution: {integrity: sha512-5WvGhztlM3la7NWf8Y6ktT+KD7zb/Hz/zdMeFjExXvEFupGvuANEnbGo1wXI4ADdSWUaRDtnQHcSGIjZ+gZ+OQ==} dependencies: - '@tabler/icons': 2.2.0 + '@tabler/icons': 2.10.0 dev: false - /@tabler/icons/2.2.0: - resolution: {integrity: sha512-s2mm+7JqmLObKdU89Dtiy+USmUpOlACsoXZZPykjAJZC4pK3wMYxLsclJxViWLeLTb6Bc0oga92V7R+9nrj1ZQ==} + /@tabler/icons/2.10.0: + resolution: {integrity: sha512-rj9xrHTSw7bPpylx8g9xhhUgO9NYKX1wGnGrMaFS5CQ9KS+jhwhKFqbZaQKhXNhpvI0cLEEW6GaRXdrC3iBs1A==} dev: false /@tensorflow/tfjs-backend-cpu/4.2.0_tkoh6rxfpzme3tc2ndqbqcrg7y: @@ -2378,6 +2366,7 @@ packages: /@tensorflow/tfjs-core/4.2.0: resolution: {integrity: sha512-uuHkiWVC8b00ngFbHvAV7J7haRlN/9PEdeenCi0CzBjgKd7aN25wPWaoN0TSQcU+GT4FJ8mofMZ9VBYZ/s/WLg==} engines: {yarn: '>= 1.3.2'} + requiresBuild: true dependencies: '@types/long': 4.0.2 '@types/offscreencanvas': 2019.7.0 @@ -2453,6 +2442,34 @@ packages: - seedrandom dev: false + /@testing-library/dom/8.20.0: + resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==} + engines: {node: '>=12'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.20.7 + '@types/aria-query': 5.0.1 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + + /@testing-library/vue/6.6.1_a2ihsjreowava2sm4iorpgwkom: + resolution: {integrity: sha512-vpyBPrHzKTwEGS7ehUC8/IXgnqTBEMk6jd52Gouf51arG2jUorPhmkbsxUwJOyxz6L0gj2ZcmWnznG1OJcTCDQ==} + engines: {node: '>=12'} + peerDependencies: + '@vue/compiler-sfc': '>= 3' + vue: '>= 3' + dependencies: + '@babel/runtime': 7.20.7 + '@testing-library/dom': 8.20.0 + '@vue/compiler-sfc': 3.2.47 + '@vue/test-utils': 2.3.0_vue@3.2.47 + vue: 3.2.47 + dev: true + /@tokenizer/token/0.3.0: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} dev: false @@ -2465,7 +2482,7 @@ packages: /@types/accepts/1.3.5: resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/archiver/5.3.1: @@ -2474,6 +2491,10 @@ packages: '@types/glob': 8.0.0 dev: true + /@types/aria-query/5.0.1: + resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} + dev: true + /@types/babel__core/7.1.20: resolution: {integrity: sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==} dependencies: @@ -2520,7 +2541,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/responselike': 1.0.0 dev: false @@ -2530,6 +2551,16 @@ packages: cbor: 8.1.0 dev: true + /@types/chai-subset/1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + dependencies: + '@types/chai': 4.3.4 + dev: true + + /@types/chai/4.3.4: + resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} + dev: true + /@types/color-convert/2.0.0: resolution: {integrity: sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==} dependencies: @@ -2562,34 +2593,34 @@ packages: /@types/fluent-ffmpeg/2.1.21: resolution: {integrity: sha512-+n3dy/Tegt6n+YwGZUiGq6i8Jrnt8+MoyPiW1L6J5EWUl7GSt18a/VyReecfCsvTTNBXNMIKOMHDstiQM8nJLA==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/glob-stream/6.1.1: resolution: {integrity: sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A==} dependencies: '@types/glob': 8.0.1 - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/glob/8.0.0: resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/glob/8.0.1: resolution: {integrity: sha512-8bVUjXZvJacUFkJXHdyZ9iH1Eaj5V7I8c4NdH5sQJsdXkqT4CA5Dhb4yb4VE/3asyx4L9ayZr1NIhTsWHczmMw==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/graceful-fs/4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/gulp-rename/2.0.1: @@ -2609,12 +2640,11 @@ packages: /@types/http-cache-semantics/4.0.1: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} - dev: false /@types/ioredis/4.28.10: resolution: {integrity: sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/istanbul-lib-coverage/2.0.4: @@ -2647,7 +2677,7 @@ packages: /@types/jsdom/21.1.0: resolution: {integrity: sha512-leWreJOdnuIxq9Y70tBVm/bvTuh31DSlF/r4l7Cfi4uhVQqLHD0Q4v301GMisEMwwbMgF7ZKxuZ+Jbd4NcdmRw==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 dev: true @@ -2671,7 +2701,7 @@ packages: /@types/keyv/3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: false /@types/long/4.0.2: @@ -2693,7 +2723,7 @@ packages: /@types/node-fetch/2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 form-data: 3.0.1 dev: false @@ -2710,19 +2740,19 @@ packages: /@types/node/18.11.18: resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} - /@types/node/18.14.1: - resolution: {integrity: sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==} + /@types/node/18.15.0: + resolution: {integrity: sha512-z6nr0TTEOBGkzLGmbypWOGnpSpSIBorEhC4L+4HeQ2iezKCi4f77kyslRwvHeNitymGQ+oFyIWGP96l/DPSV9w==} /@types/nodemailer/6.4.7: resolution: {integrity: sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/oauth/0.9.1: resolution: {integrity: sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/offscreencanvas/2019.3.0: @@ -2736,7 +2766,7 @@ packages: /@types/pg/8.6.6: resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 pg-protocol: 1.5.0 pg-types: 2.2.0 dev: true @@ -2756,7 +2786,7 @@ packages: /@types/qrcode/1.5.0: resolution: {integrity: sha512-x5ilHXRxUPIMfjtM+1vf/GPTRWZ81nqscursm5gMznJeK9M0YnZ1c3bEvRLQ0zSSgedLx1J6MGL231ObQGGhaA==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/random-seed/0.3.3: @@ -2780,11 +2810,11 @@ packages: /@types/responselike/1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: false - /@types/sanitize-html/2.8.0: - resolution: {integrity: sha512-Uih6caOm3DsBYnVGOYn0A9NoTNe1c4aPStmHC/YA2JrpP9kx//jzaRcIklFvSpvVQEcpl/ZCr4DgISSf/YxTvg==} + /@types/sanitize-html/2.8.1: + resolution: {integrity: sha512-Q6kMAbBBaXA5IagoipeSr4Y/zuGyh4BZ5lewgb3cYe3OYqy0k/d67iMsC4O895eks676bVAe9G+0y1i0k2ZlnA==} dependencies: htmlparser2: 8.0.1 dev: true @@ -2808,7 +2838,7 @@ packages: /@types/sharp/0.31.1: resolution: {integrity: sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/sinonjs__fake-timers/8.1.1: @@ -2850,7 +2880,7 @@ packages: /@types/undertaker/1.2.8: resolution: {integrity: sha512-gW3PRqCHYpo45XFQHJBhch7L6hytPsIe0QeLujlnFsjHPnXLhJcPdN6a9368d7aIQgH2I/dUTPFBlGeSNA3qOg==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/undertaker-registry': 1.0.1 async-done: 1.3.2 dev: true @@ -2858,7 +2888,7 @@ packages: /@types/unzipper/0.10.5: resolution: {integrity: sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/uuid/9.0.1: @@ -2868,14 +2898,14 @@ packages: /@types/vary/1.1.0: resolution: {integrity: sha512-LQWqrIa0dvEOOH37lGksMEXbypRLUFqu6Gx0pmX7zIUisD2I/qaVgEX/vJ/PSVSW0Hk6yz1BNkFpqg6dZm3Wug==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/vinyl-fs/2.4.12: resolution: {integrity: sha512-LgBpYIWuuGsihnlF+OOWWz4ovwCYlT03gd3DuLwex50cYZLmX3yrW+sFF9ndtmh7zcZpS6Ri47PrIu+fV+sbXw==} dependencies: '@types/glob-stream': 6.1.1 - '@types/node': 18.14.1 + '@types/node': 18.15.0 '@types/vinyl': 2.0.7 dev: true @@ -2883,12 +2913,12 @@ packages: resolution: {integrity: sha512-4UqPv+2567NhMQuMLdKAyK4yzrfCqwaTt6bLhHEs8PFcxbHILsrxaY63n4wgE/BRLDWDQeI+WcTmkXKExh9hQg==} dependencies: '@types/expect': 1.20.4 - '@types/node': 18.14.1 + '@types/node': 18.15.0 /@types/web-push/3.3.2: resolution: {integrity: sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/webgl-ext/0.0.30: @@ -2898,13 +2928,13 @@ packages: /@types/websocket/1.0.5: resolution: {integrity: sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/ws/8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true /@types/yargs-parser/21.0.0: @@ -2927,40 +2957,12 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 dev: true optional: true - /@typescript-eslint/eslint-plugin/5.52.0_cjo54hduev4bqhpjw5znwiokqu: - resolution: {integrity: sha512-lHazYdvYVsBokwCdKOppvYJKaJ4S41CgKBcPvyd0xjZNbvQdhn/pnJlGtQksQ/NhInzdaeaSarlBjDXHuclEbg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/parser': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu - '@typescript-eslint/scope-manager': 5.52.0 - '@typescript-eslint/type-utils': 5.52.0_ycpbpc6yetojsgtrx3mwntkhsu - '@typescript-eslint/utils': 5.52.0_ycpbpc6yetojsgtrx3mwntkhsu - debug: 4.3.4 - eslint: 8.35.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - natural-compare-lite: 1.4.0 - regexpp: 3.2.0 - semver: 7.3.8 - tsutils: 3.21.0_typescript@4.9.5 - typescript: 4.9.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/eslint-plugin/5.53.0_cjo54hduev4bqhpjw5znwiokqu: - resolution: {integrity: sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw==} + /@typescript-eslint/eslint-plugin/5.54.1_mlk7dnz565t663n4razh6a6v6i: + resolution: {integrity: sha512-a2RQAkosH3d3ZIV08s3DcL/mcGc2M/UC528VkPULFxR9VnVPT8pBu0IyBAJJmVsCmhVfwQX1v6q+QGnmSe1bew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 @@ -2970,10 +2972,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu - '@typescript-eslint/scope-manager': 5.53.0 - '@typescript-eslint/type-utils': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu - '@typescript-eslint/utils': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu + '@typescript-eslint/parser': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu + '@typescript-eslint/scope-manager': 5.54.1 + '@typescript-eslint/type-utils': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu + '@typescript-eslint/utils': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu debug: 4.3.4 eslint: 8.35.0 grapheme-splitter: 1.0.4 @@ -3007,8 +3009,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser/5.53.0_ycpbpc6yetojsgtrx3mwntkhsu: - resolution: {integrity: sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ==} + /@typescript-eslint/parser/5.54.1_ycpbpc6yetojsgtrx3mwntkhsu: + resolution: {integrity: sha512-8zaIXJp/nG9Ff9vQNh7TI+C3nA6q6iIsGJ4B4L6MhZ7mHnTMR4YP5vp2xydmFXIy8rpyIVbNAG44871LMt6ujg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3017,9 +3019,9 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 5.53.0 - '@typescript-eslint/types': 5.53.0 - '@typescript-eslint/typescript-estree': 5.53.0_typescript@4.9.5 + '@typescript-eslint/scope-manager': 5.54.1 + '@typescript-eslint/types': 5.54.1 + '@typescript-eslint/typescript-estree': 5.54.1_typescript@4.9.5 debug: 4.3.4 eslint: 8.35.0 typescript: 4.9.5 @@ -3035,16 +3037,16 @@ packages: '@typescript-eslint/visitor-keys': 5.52.0 dev: true - /@typescript-eslint/scope-manager/5.53.0: - resolution: {integrity: sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==} + /@typescript-eslint/scope-manager/5.54.1: + resolution: {integrity: sha512-zWKuGliXxvuxyM71UA/EcPxaviw39dB2504LqAmFDjmkpO8qNLHcmzlh6pbHs1h/7YQ9bnsO8CCcYCSA8sykUg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.53.0 - '@typescript-eslint/visitor-keys': 5.53.0 + '@typescript-eslint/types': 5.54.1 + '@typescript-eslint/visitor-keys': 5.54.1 dev: true - /@typescript-eslint/type-utils/5.52.0_ycpbpc6yetojsgtrx3mwntkhsu: - resolution: {integrity: sha512-tEKuUHfDOv852QGlpPtB3lHOoig5pyFQN/cUiZtpw99D93nEBjexRLre5sQZlkMoHry/lZr8qDAt2oAHLKA6Jw==} + /@typescript-eslint/type-utils/5.54.1_ycpbpc6yetojsgtrx3mwntkhsu: + resolution: {integrity: sha512-WREHsTz0GqVYLIbzIZYbmUUr95DKEKIXZNH57W3s+4bVnuF1TKe2jH8ZNH8rO1CeMY3U4j4UQeqPNkHMiGem3g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -3053,28 +3055,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.52.0_typescript@4.9.5 - '@typescript-eslint/utils': 5.52.0_ycpbpc6yetojsgtrx3mwntkhsu - debug: 4.3.4 - eslint: 8.35.0 - tsutils: 3.21.0_typescript@4.9.5 - typescript: 4.9.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/type-utils/5.53.0_ycpbpc6yetojsgtrx3mwntkhsu: - resolution: {integrity: sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.53.0_typescript@4.9.5 - '@typescript-eslint/utils': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu + '@typescript-eslint/typescript-estree': 5.54.1_typescript@4.9.5 + '@typescript-eslint/utils': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu debug: 4.3.4 eslint: 8.35.0 tsutils: 3.21.0_typescript@4.9.5 @@ -3088,8 +3070,8 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/types/5.53.0: - resolution: {integrity: sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==} + /@typescript-eslint/types/5.54.1: + resolution: {integrity: sha512-G9+1vVazrfAfbtmCapJX8jRo2E4MDXxgm/IMOF4oGh3kq7XuK3JRkOg6y2Qu1VsTRmWETyTkWt1wxy7X7/yLkw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -3114,8 +3096,8 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree/5.53.0_typescript@4.9.5: - resolution: {integrity: sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==} + /@typescript-eslint/typescript-estree/5.54.1_typescript@4.9.5: + resolution: {integrity: sha512-bjK5t+S6ffHnVwA0qRPTZrxKSaFYocwFIkZx5k7pvWfsB1I57pO/0M0Skatzzw1sCkjJ83AfGTL0oFIFiDX3bg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -3123,8 +3105,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 5.53.0 - '@typescript-eslint/visitor-keys': 5.53.0 + '@typescript-eslint/types': 5.54.1 + '@typescript-eslint/visitor-keys': 5.54.1 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -3135,37 +3117,17 @@ packages: - supports-color dev: true - /@typescript-eslint/utils/5.52.0_ycpbpc6yetojsgtrx3mwntkhsu: - resolution: {integrity: sha512-As3lChhrbwWQLNk2HC8Ree96hldKIqk98EYvypd3It8Q1f8d5zWyIoaZEp2va5667M4ZyE7X8UUR+azXrFl+NA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.11 - '@types/semver': 7.3.13 - '@typescript-eslint/scope-manager': 5.52.0 - '@typescript-eslint/types': 5.52.0 - '@typescript-eslint/typescript-estree': 5.52.0_typescript@4.9.5 - eslint: 8.35.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.35.0 - semver: 7.3.8 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/utils/5.53.0_ycpbpc6yetojsgtrx3mwntkhsu: - resolution: {integrity: sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g==} + /@typescript-eslint/utils/5.54.1_ycpbpc6yetojsgtrx3mwntkhsu: + resolution: {integrity: sha512-IY5dyQM8XD1zfDe5X8jegX6r2EVU5o/WJnLu/znLPWCBF7KNGC+adacXnt5jEYS9JixDcoccI6CvE4RCjHMzCQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: '@types/json-schema': 7.0.11 '@types/semver': 7.3.13 - '@typescript-eslint/scope-manager': 5.53.0 - '@typescript-eslint/types': 5.53.0 - '@typescript-eslint/typescript-estree': 5.53.0_typescript@4.9.5 + '@typescript-eslint/scope-manager': 5.54.1 + '@typescript-eslint/types': 5.54.1 + '@typescript-eslint/typescript-estree': 5.54.1_typescript@4.9.5 eslint: 8.35.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0_eslint@8.35.0 @@ -3183,11 +3145,11 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@typescript-eslint/visitor-keys/5.53.0: - resolution: {integrity: sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==} + /@typescript-eslint/visitor-keys/5.54.1: + resolution: {integrity: sha512-q8iSoHTgwCfgcRJ2l2x+xCbu8nBlRAlsQ33k24Adj8eoVBE0f8dUeI+bAa8F84Mv05UGbAx57g2zrRsYIooqQg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.53.0 + '@typescript-eslint/types': 5.54.1 eslint-visitor-keys: 3.3.0 dev: true @@ -3198,10 +3160,53 @@ packages: vite: ^4.0.0 vue: ^3.2.25 dependencies: - vite: 4.1.4_435aevtanapkguv7m72cl6trbi + vite: 4.1.4_6e4omgvd5jf4hig7wpb5tmdc3q vue: 3.2.47 dev: false + /@vitest/coverage-c8/0.29.2_vitest@0.29.2: + resolution: {integrity: sha512-NmD3WirQCeQjjKfHu4iEq18DVOBFbLn9TKVdMpyi5YW2EtnS+K22/WE+9/wRrepOhyeTxuEFgxUVkCAE1GhbnQ==} + peerDependencies: + vitest: '>=0.29.0 <1' + dependencies: + c8: 7.13.0 + picocolors: 1.0.0 + std-env: 3.3.2 + vitest: 0.29.2_zcjcryjt4bqcdu7ggonulipgea + dev: true + + /@vitest/expect/0.29.2: + resolution: {integrity: sha512-wjrdHB2ANTch3XKRhjWZN0UueFocH0cQbi2tR5Jtq60Nb3YOSmakjdAvUa2JFBu/o8Vjhj5cYbcMXkZxn1NzmA==} + dependencies: + '@vitest/spy': 0.29.2 + '@vitest/utils': 0.29.2 + chai: 4.3.7 + dev: true + + /@vitest/runner/0.29.2: + resolution: {integrity: sha512-A1P65f5+6ru36AyHWORhuQBJrOOcmDuhzl5RsaMNFe2jEkoj0faEszQS4CtPU/LxUYVIazlUtZTY0OEZmyZBnA==} + dependencies: + '@vitest/utils': 0.29.2 + p-limit: 4.0.0 + pathe: 1.1.0 + dev: true + + /@vitest/spy/0.29.2: + resolution: {integrity: sha512-Hc44ft5kaAytlGL2PyFwdAsufjbdOvHklwjNy/gy/saRbg9Kfkxfh+PklLm1H2Ib/p586RkQeNFKYuJInUssyw==} + dependencies: + tinyspy: 1.1.1 + dev: true + + /@vitest/utils/0.29.2: + resolution: {integrity: sha512-F14/Uc+vCdclStS2KEoXJlOLAEyqRhnw0gM27iXw9bMTcyKRPJrQ+rlC6XZ125GIPvvKYMPpVxNhiou6PsEeYQ==} + dependencies: + cli-truncate: 3.1.0 + diff: 5.1.0 + loupe: 2.3.6 + picocolors: 1.0.0 + pretty-format: 27.5.1 + dev: true + /@volar/language-core/1.3.0-alpha.0: resolution: {integrity: sha512-W3uMzecHPcbwddPu4SJpUcPakRBK/y/BP+U0U6NiPpUX1tONLC4yCawt+QBJqtgJ+sfD6ztf5PyvPL3hQRqfOA==} dependencies: @@ -3309,7 +3314,6 @@ packages: '@vue/runtime-core': 3.2.47 '@vue/shared': 3.2.47 csstype: 2.6.21 - dev: false /@vue/server-renderer/3.2.47_vue@3.2.47: resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==} @@ -3319,11 +3323,22 @@ packages: '@vue/compiler-ssr': 3.2.47 '@vue/shared': 3.2.47 vue: 3.2.47 - dev: false /@vue/shared/3.2.47: resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==} + /@vue/test-utils/2.3.0_vue@3.2.47: + resolution: {integrity: sha512-S8/9Z+B4VSsTUNtZtzS7J1TfxJbf10n+gcH9X8cASbG0Tp7qD6vqs/sUNlmpzk6i7+pP00ptauJp9rygyW89Ww==} + peerDependencies: + vue: ^3.0.1 + dependencies: + js-beautify: 1.14.6 + vue: 3.2.47 + optionalDependencies: + '@vue/compiler-dom': 3.2.47 + '@vue/server-renderer': 3.2.47_vue@3.2.47 + dev: true + /@webgpu/types/0.1.21: resolution: {integrity: sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==} dev: false @@ -3334,7 +3349,6 @@ packages: /abbrev/1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: false /abort-controller/3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -3362,18 +3376,17 @@ packages: acorn-walk: 8.2.0 dev: false - /acorn-jsx/5.3.2_acorn@8.8.1: + /acorn-jsx/5.3.2_acorn@8.8.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.8.1 + acorn: 8.8.2 dev: true /acorn-walk/8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} - dev: false /acorn/7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} @@ -3386,6 +3399,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + /acorn/8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + /adm-zip/0.5.10: resolution: {integrity: sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==} engines: {node: '>=6.0'} @@ -3494,6 +3512,11 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex/6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + /ansi-styles/2.2.1: resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} engines: {node: '>=0.10.0'} @@ -3517,6 +3540,11 @@ packages: engines: {node: '>=10'} dev: true + /ansi-styles/6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /ansi-wrap/0.1.0: resolution: {integrity: sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==} engines: {node: '>=0.10.0'} @@ -3623,6 +3651,12 @@ packages: /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + /aria-query/5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + dependencies: + deep-equal: 2.2.0 + dev: true + /arr-diff/4.0.0: resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} engines: {node: '>=0.10.0'} @@ -3664,7 +3698,7 @@ packages: call-bind: 1.0.2 define-properties: 1.1.4 es-abstract: 1.20.4 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.0 is-string: 1.0.7 dev: true @@ -3752,6 +3786,10 @@ packages: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} + /assertion-error/1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /assign-symbols/1.0.0: resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} engines: {node: '>=0.10.0'} @@ -3828,7 +3866,6 @@ packages: /available-typed-arrays/1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - dev: false /avvio/8.2.0: resolution: {integrity: sha512-bbCQdg7bpEv6kGH41RO/3B2/GMMmJSo2iBK+X8AWN9mujtfUipMDfIjsgHCfpnKqoGEQrrmCDKSa5OQ19+fDmg==} @@ -3879,17 +3916,17 @@ packages: - debug dev: true - /babel-jest/29.4.3_@babel+core@7.20.12: - resolution: {integrity: sha512-o45Wyn32svZE+LnMVWv/Z4x0SwtLbh4FyGcYtR20kIWd+rdrDZ9Fzq8Ml3MYLD+mZvEdzCjZsCnYZ2jpJyQ+Nw==} + /babel-jest/29.5.0_@babel+core@7.20.12: + resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: '@babel/core': 7.20.12 - '@jest/transform': 29.4.3 + '@jest/transform': 29.5.0 '@types/babel__core': 7.1.20 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.4.3_@babel+core@7.20.12 + babel-preset-jest: 29.5.0_@babel+core@7.20.12 chalk: 4.1.2 graceful-fs: 4.2.10 slash: 3.0.0 @@ -3910,8 +3947,8 @@ packages: - supports-color dev: true - /babel-plugin-jest-hoist/29.4.3: - resolution: {integrity: sha512-mB6q2q3oahKphy5V7CpnNqZOCkxxZ9aokf1eh82Dy3jQmg4xvM1tGrh5y6BQUJh4a3Pj9+eLfwvAZ7VNKg7H8Q==} + /babel-plugin-jest-hoist/29.5.0: + resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.20.7 @@ -3940,14 +3977,14 @@ packages: '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.20.12 dev: true - /babel-preset-jest/29.4.3_@babel+core@7.20.12: - resolution: {integrity: sha512-gWx6COtSuma6n9bw+8/F+2PCXrIgxV/D1TJFnp6OyBK2cxPWg0K9p/sriNYeifKjpUkMViWQ09DSWtzJQRETsw==} + /babel-preset-jest/29.5.0_@babel+core@7.20.12: + resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.20.12 - babel-plugin-jest-hoist: 29.4.3 + babel-plugin-jest-hoist: 29.5.0 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.12 dev: true @@ -4129,6 +4166,8 @@ packages: /browserslist/1.7.7: resolution: {integrity: sha512-qHJblDE2bXVRYzuDetv/wAeHOJyO97+9wxC1cdCtyzgNuSozOyRCiiLaCR1f71AN66lQdVVBipWm63V+a7bPOw==} + deprecated: Browserslist 2 could fail on reading Browserslist >3.0 config used in other tools. + hasBin: true dependencies: caniuse-db: 1.0.30001443 electron-to-chromium: 1.4.284 @@ -4137,6 +4176,7 @@ packages: /browserslist/4.21.4: resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true dependencies: caniuse-lite: 1.0.30001443 electron-to-chromium: 1.4.284 @@ -4231,6 +4271,30 @@ packages: streamsearch: 1.1.0 dev: false + /c8/7.13.0: + resolution: {integrity: sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==} + engines: {node: '>=10.12.0'} + hasBin: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 2.0.0 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-report: 3.0.0 + istanbul-reports: 3.1.5 + rimraf: 3.0.2 + test-exclude: 6.0.0 + v8-to-istanbul: 9.0.1 + yargs: 16.2.0 + yargs-parser: 20.2.9 + dev: true + + /cac/6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /cacache/16.1.3: resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -4285,20 +4349,18 @@ packages: /cacheable-lookup/7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} - dev: false - /cacheable-request/10.2.5: - resolution: {integrity: sha512-5RwYYCfzjNPsyJxb/QpaM0bfzx+kw5/YpDhZPm9oMIDntHFQ9YXeyV47ZvzlTE0XrrrbyO2UITJH4GF9eRLdXQ==} + /cacheable-request/10.2.8: + resolution: {integrity: sha512-IDVO5MJ4LItE6HKFQTqT2ocAQsisOoCTUDu1ddCmnhyiwFQjXNPp4081Xj23N4tO+AFEFNzGuNEf/c8Gwwt15A==} engines: {node: '>=14.16'} dependencies: '@types/http-cache-semantics': 4.0.1 get-stream: 6.0.1 - http-cache-semantics: 4.1.0 + http-cache-semantics: 4.1.1 keyv: 4.5.2 mimic-response: 4.0.0 normalize-url: 8.0.0 responselike: 3.0.0 - dev: false /cacheable-request/7.0.2: resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==} @@ -4306,7 +4368,7 @@ packages: dependencies: clone-response: 1.0.3 get-stream: 5.2.0 - http-cache-semantics: 4.1.0 + http-cache-semantics: 4.1.1 keyv: 4.5.2 lowercase-keys: 2.0.0 normalize-url: 6.1.0 @@ -4377,6 +4439,19 @@ packages: dependencies: nofilter: 3.1.0 + /chai/4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chainsaw/0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} dependencies: @@ -4474,6 +4549,10 @@ packages: hammerjs: 2.0.8 dev: false + /check-error/1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + /check-more-types/2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -4488,7 +4567,6 @@ packages: domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.0.1 - dev: false /cheerio/1.0.0-rc.12: resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} @@ -4501,7 +4579,6 @@ packages: htmlparser2: 8.0.1 parse5: 7.1.2 parse5-htmlparser2-tree-adapter: 7.0.0 - dev: false /chokidar/3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} @@ -4592,6 +4669,14 @@ packages: string-width: 4.2.3 dev: true + /cli-truncate/3.1.0: + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + slice-ansi: 5.0.0 + string-width: 5.1.2 + dev: true + /cliui/3.2.0: resolution: {integrity: sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==} dependencies: @@ -4614,7 +4699,6 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: false /cliui/8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} @@ -4779,7 +4863,6 @@ packages: /commander/2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - dev: false /commander/5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} @@ -4832,6 +4915,13 @@ packages: typedarray: 0.0.6 dev: false + /config-chain/1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: true + /consola/2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} dev: false @@ -4923,6 +5013,14 @@ packages: cross-spawn: 7.0.3 dev: true + /cross-fetch/3.1.5: + resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} + dependencies: + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + dev: true + /cross-spawn/5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -4951,16 +5049,13 @@ packages: domhandler: 5.0.3 domutils: 3.0.1 nth-check: 2.1.1 - dev: false /css-what/6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} - dev: false /css.escape/1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - dev: false /cssesc/3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} @@ -5029,7 +5124,6 @@ packages: /csstype/2.6.21: resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} - dev: false /csstype/3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} @@ -5234,7 +5328,6 @@ packages: engines: {node: '>=10'} dependencies: mimic-response: 3.1.0 - dev: false /dedent/0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} @@ -5251,6 +5344,35 @@ packages: - debug dev: false + /deep-eql/4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-equal/2.2.0: + resolution: {integrity: sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==} + dependencies: + call-bind: 1.0.2 + es-get-iterator: 1.1.3 + get-intrinsic: 1.1.3 + is-arguments: 1.1.1 + is-array-buffer: 3.0.2 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + isarray: 2.0.5 + object-is: 1.1.5 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.9 + dev: true + /deep-extend/0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -5278,7 +5400,6 @@ packages: /defer-to-connect/2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} - dev: false /define-properties/1.1.4: resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} @@ -5360,6 +5481,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff/5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + dev: true + /dijkstrajs/1.0.2: resolution: {integrity: sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==} dev: false @@ -5392,6 +5518,10 @@ packages: resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} dev: false + /dom-accessibility-api/0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + /dom-serializer/2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} dependencies: @@ -5453,6 +5583,10 @@ packages: object.defaults: 1.1.0 dev: false + /eastasianwidth/0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /ecc-jsbn/0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} dependencies: @@ -5465,9 +5599,20 @@ packages: safe-buffer: 5.2.1 dev: false + /editorconfig/0.15.3: + resolution: {integrity: sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==} + hasBin: true + dependencies: + commander: 2.20.3 + lru-cache: 4.1.5 + semver: 5.7.1 + sigmund: 1.0.1 + dev: true + /ejs/3.1.8: resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} engines: {node: '>=0.10.0'} + hasBin: true dependencies: jake: 10.8.5 dev: false @@ -5483,6 +5628,10 @@ packages: /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /emoji-regex/9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /encode-utf8/1.0.3: resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} dev: false @@ -5537,12 +5686,12 @@ packages: es-to-primitive: 1.2.1 function-bind: 1.1.1 function.prototype.name: 1.1.5 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.0 get-symbol-description: 1.0.0 has: 1.0.3 has-property-descriptors: 1.0.0 has-symbols: 1.0.3 - internal-slot: 1.0.3 + internal-slot: 1.0.5 is-callable: 1.2.7 is-negative-zero: 2.0.2 is-regex: 1.1.4 @@ -5559,6 +5708,20 @@ packages: unbox-primitive: 1.0.2 dev: true + /es-get-iterator/1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: true + /es-shim-unscopables/1.0.0: resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} dependencies: @@ -5855,7 +6018,6 @@ packages: '@esbuild/win32-arm64': 0.16.17 '@esbuild/win32-ia32': 0.16.17 '@esbuild/win32-x64': 0.16.17 - dev: false /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -5867,7 +6029,6 @@ packages: /escape-regexp/0.0.1: resolution: {integrity: sha512-jVgdsYRa7RKxTT6MKNC3gdT+BF0Gfhpel19+HMRZJC2L0PufB0XOBuXBoXj29NKHwuktnAXd1Z1lyiH/8vOTpw==} - dev: false /escape-string-regexp/1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -5909,7 +6070,7 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.4_bchzgevzrq32s4jgdbchl2wqu4: + /eslint-module-utils/2.7.4_npjqex3ey3rgd34fjcuucz7la4: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -5930,15 +6091,15 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu + '@typescript-eslint/parser': 5.52.0_7kw3g6rralp5ps6mg3uyzz6azm debug: 3.2.7 - eslint: 8.35.0 + eslint: 8.34.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: - supports-color dev: true - /eslint-module-utils/2.7.4_npjqex3ey3rgd34fjcuucz7la4: + /eslint-module-utils/2.7.4_spn4godk7g7ml4zhqabnc6rdgi: resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -5959,9 +6120,9 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.52.0_7kw3g6rralp5ps6mg3uyzz6azm + '@typescript-eslint/parser': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu debug: 3.2.7 - eslint: 8.34.0 + eslint: 8.35.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: - supports-color @@ -6000,7 +6161,7 @@ packages: - supports-color dev: true - /eslint-plugin-import/2.27.5_nhka4er4oejxhxq3ecgtwxvdji: + /eslint-plugin-import/2.27.5_uyiasnnzcqrxqkfvjklwnmwcha: resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} peerDependencies: @@ -6010,7 +6171,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.53.0_ycpbpc6yetojsgtrx3mwntkhsu + '@typescript-eslint/parser': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -6018,7 +6179,7 @@ packages: doctrine: 2.1.0 eslint: 8.35.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4_bchzgevzrq32s4jgdbchl2wqu4 + eslint-module-utils: 2.7.4_spn4godk7g7ml4zhqabnc6rdgi has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -6197,19 +6358,21 @@ packages: resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.8.1 - acorn-jsx: 5.3.2_acorn@8.8.1 + acorn: 8.8.2 + acorn-jsx: 5.3.2_acorn@8.8.2 eslint-visitor-keys: 3.3.0 dev: true /esprima/2.7.3: resolution: {integrity: sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==} engines: {node: '>=0.10.0'} + hasBin: true dev: false /esprima/4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} + hasBin: true /esquery/1.4.0: resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} @@ -6390,18 +6553,18 @@ packages: jest-get-type: 29.2.0 jest-matcher-utils: 29.3.1 jest-message-util: 29.3.1 - jest-util: 29.4.2 + jest-util: 29.5.0 dev: true - /expect/29.4.3: - resolution: {integrity: sha512-uC05+Q7eXECFpgDrHdXA4k2rpMyStAYPItEDLyQDo5Ta7fVkJnNA/4zh/OIVkVVNZ1oOK1PipQoyNjuZ6sz6Dg==} + /expect/29.5.0: + resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/expect-utils': 29.4.3 + '@jest/expect-utils': 29.5.0 jest-get-type: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-util: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 dev: true /ext-list/2.2.2: @@ -6558,8 +6721,8 @@ packages: resolution: {integrity: sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==} dev: false - /fastify/4.13.0: - resolution: {integrity: sha512-p9ibdFWH3pZ7KPgmfHPKGUy2W4EWU2TEpwlcu58w4CwGyU3ARFfh2kwq6zpZ5W2ZGVbufi4tZbqHIHAlX/9Z/A==} + /fastify/4.14.1: + resolution: {integrity: sha512-yjrDeXe77j9gRlSV2UJry8mcFWbD0NQ5JYjnPi4tkFjHZVaG3/BD5wxOmRzGnHPC0YvaBJ0XWrIfFPl2IHRa1w==} dependencies: '@fastify/ajv-compiler': 3.5.0 '@fastify/error': 3.2.0 @@ -6804,7 +6967,6 @@ packages: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 - dev: false /for-in/1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} @@ -6818,13 +6980,20 @@ packages: for-in: 1.0.2 dev: false + /foreground-child/2.0.0: + resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} + engines: {node: '>=8.0.0'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 3.0.7 + dev: true + /forever-agent/0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} /form-data-encoder/2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} - dev: false /form-data/2.3.3: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} @@ -7003,6 +7172,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + /get-func-name/2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + /get-intrinsic/1.1.3: resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} dependencies: @@ -7010,6 +7183,13 @@ packages: has: 1.0.3 has-symbols: 1.0.3 + /get-intrinsic/1.2.0: + resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + /get-package-type/0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -7055,7 +7235,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.0 dev: true /get-value/2.0.6: @@ -7155,7 +7335,6 @@ packages: inherits: 2.0.4 minimatch: 5.1.2 once: 1.4.0 - dev: false /global-dirs/3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} @@ -7222,8 +7401,7 @@ packages: /gopd/1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.1.3 - dev: false + get-intrinsic: 1.2.0 /got/11.8.5: resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==} @@ -7242,14 +7420,14 @@ packages: responselike: 2.0.1 dev: false - /got/12.5.3: - resolution: {integrity: sha512-8wKnb9MGU8IPGRIo+/ukTy9XLJBwDiCpIf5TVzQ9Cpol50eMTpBq2GAuDsuDIz7hTYmZgMgC1e9ydr6kSDWs3w==} + /got/12.6.0: + resolution: {integrity: sha512-WTcaQ963xV97MN3x0/CbAriXFZcXCfgxVp91I+Ze6pawQOa7SgzwSx2zIJJsX+kTajMnVs0xcFD1TxZKFqhdnQ==} engines: {node: '>=14.16'} dependencies: '@sindresorhus/is': 5.3.0 '@szmarczak/http-timer': 5.0.1 cacheable-lookup: 7.0.0 - cacheable-request: 10.2.5 + cacheable-request: 10.2.8 decompress-response: 6.0.0 form-data-encoder: 2.1.4 get-stream: 6.0.1 @@ -7257,7 +7435,6 @@ packages: lowercase-keys: 3.0.0 p-cancelable: 3.0.0 responselike: 3.0.0 - dev: false /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} @@ -7368,7 +7545,6 @@ packages: whatwg-mimetype: 3.0.0 transitivePeerDependencies: - encoding - dev: false /har-schema/2.0.0: resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} @@ -7411,7 +7587,7 @@ packages: /has-property-descriptors/1.0.0: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} dependencies: - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.0 /has-symbols/1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} @@ -7510,7 +7686,6 @@ packages: /html-entities/2.3.2: resolution: {integrity: sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==} - dev: false /html-escaper/2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -7524,9 +7699,8 @@ packages: domutils: 3.0.1 entities: 4.4.0 - /http-cache-semantics/4.1.0: - resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} - dev: false + /http-cache-semantics/4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} /http-errors/2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} @@ -7582,7 +7756,6 @@ packages: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - dev: false /http_ece/1.1.0: resolution: {integrity: sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==} @@ -7644,7 +7817,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 - dev: false /idb-keyval/6.2.0: resolution: {integrity: sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==} @@ -7665,7 +7837,6 @@ packages: /immutable/4.2.2: resolution: {integrity: sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==} - dev: false /import-fresh/3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -7678,6 +7849,7 @@ packages: /import-local/3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} + hasBin: true dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 @@ -7710,7 +7882,6 @@ packages: /ini/1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: false /ini/2.0.0: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} @@ -7725,11 +7896,11 @@ packages: resolution: {integrity: sha512-yCFcLvqk0yQdxx0uJz4t9Z3adDMLAYrcGYv546uRXCSvxE+GqNYhhz/KmrGcUKGI/gVLR9n/e/zM9jX/+ASMJQ==} dev: false - /internal-slot/1.0.3: - resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} + /internal-slot/1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.0 has: 1.0.3 side-channel: 1.0.4 dev: true @@ -7802,7 +7973,6 @@ packages: /ip-regex/4.3.0: resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} engines: {node: '>=8'} - dev: false /ip-regex/5.0.0: resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==} @@ -7821,7 +7991,6 @@ packages: /ipaddr.js/2.0.1: resolution: {integrity: sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==} engines: {node: '>= 10'} - dev: false /is-absolute-url/2.1.0: resolution: {integrity: sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==} @@ -7856,7 +8025,14 @@ packages: dependencies: call-bind: 1.0.2 has-tostringtag: 1.0.0 - dev: false + + /is-array-buffer/3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-typed-array: 1.1.10 + dev: true /is-arrayish/0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -7982,6 +8158,11 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + /is-fullwidth-code-point/4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + dev: true + /is-generator-fn/2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -8020,12 +8201,15 @@ packages: engines: {node: '>=8'} dependencies: ip-regex: 4.3.0 - dev: false /is-lambda/1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} dev: false + /is-map/2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: true + /is-negated-glob/1.0.0: resolution: {integrity: sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==} engines: {node: '>=0.10.0'} @@ -8103,6 +8287,10 @@ packages: is-unc-path: 1.0.0 dev: false + /is-set/2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: true + /is-shared-array-buffer/1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: @@ -8160,7 +8348,6 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 - dev: false /is-typedarray/1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -8186,12 +8373,23 @@ packages: engines: {node: '>=0.10.0'} dev: false + /is-weakmap/2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + dev: true + /is-weakref/1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: call-bind: 1.0.2 dev: true + /is-weakset/2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + dev: true + /is-windows/1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -8205,6 +8403,10 @@ packages: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: false + /isarray/2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -8285,6 +8487,7 @@ packages: /jake/10.8.5: resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} engines: {node: '>=10'} + hasBin: true dependencies: async: 3.2.4 chalk: 4.1.2 @@ -8292,43 +8495,44 @@ packages: minimatch: 3.1.2 dev: false - /jest-changed-files/29.4.3: - resolution: {integrity: sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ==} + /jest-changed-files/29.5.0: + resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: execa: 5.1.1 p-limit: 3.1.0 dev: true - /jest-circus/29.4.3: - resolution: {integrity: sha512-Vw/bVvcexmdJ7MLmgdT3ZjkJ3LKu8IlpefYokxiqoZy6OCQ2VAm6Vk3t/qHiAGUXbdbJKJWnc8gH3ypTbB/OBw==} + /jest-circus/29.5.0: + resolution: {integrity: sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/expect': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 18.14.1 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 is-generator-fn: 2.1.0 - jest-each: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-runtime: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 + jest-each: 29.5.0 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 p-limit: 3.1.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + pure-rand: 6.0.0 slash: 3.0.0 stack-utils: 2.0.6 transitivePeerDependencies: - supports-color dev: true - /jest-cli/29.4.3_@types+node@18.14.1: - resolution: {integrity: sha512-PiiAPuFNfWWolCE6t3ZrDXQc6OsAuM3/tVW0u27UWc1KE+n/HSn5dSE6B2juqN7WP+PP0jAcnKtGmI4u8GMYCg==} + /jest-cli/29.5.0_@types+node@18.15.0: + resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -8337,16 +8541,16 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 + '@jest/core': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 29.4.3_@types+node@18.14.1 - jest-util: 29.4.3 - jest-validate: 29.4.3 + jest-config: 29.5.0_@types+node@18.15.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 prompts: 2.4.2 yargs: 17.6.2 transitivePeerDependencies: @@ -8355,8 +8559,8 @@ packages: - ts-node dev: true - /jest-config/29.4.3_@types+node@18.14.1: - resolution: {integrity: sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ==} + /jest-config/29.5.0_@types+node@18.15.0: + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -8368,50 +8572,40 @@ packages: optional: true dependencies: '@babel/core': 7.20.12 - '@jest/test-sequencer': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 18.14.1 - babel-jest: 29.4.3_@babel+core@7.20.12 + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 + babel-jest: 29.5.0_@babel+core@7.20.12 chalk: 4.1.2 ci-info: 3.7.1 deepmerge: 4.2.2 glob: 7.2.3 graceful-fs: 4.2.10 - jest-circus: 29.4.3 - jest-environment-node: 29.4.3 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 jest-get-type: 29.4.3 jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-runner: 29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color dev: true - /jest-diff/29.4.2: - resolution: {integrity: sha512-EK8DSajVtnjx9sa1BkjZq3mqChm2Cd8rIzdXkQMA8e0wuXq53ypz6s5o5V8HRZkoEt2ywJ3eeNWFKWeYr8HK4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - chalk: 4.1.2 - diff-sequences: 29.4.3 - jest-get-type: 29.4.3 - pretty-format: 29.4.3 - dev: true - - /jest-diff/29.4.3: - resolution: {integrity: sha512-YB+ocenx7FZ3T5O9lMVMeLYV4265socJKtkwgk/6YUz/VsEzYDkiMuMhWzZmxm3wDRQvayJu/PjkjjSkjoHsCA==} + /jest-diff/29.5.0: + resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 diff-sequences: 29.4.3 jest-get-type: 29.4.3 - pretty-format: 29.4.3 + pretty-format: 29.5.0 dev: true /jest-docblock/29.4.3: @@ -8421,27 +8615,27 @@ packages: detect-newline: 3.1.0 dev: true - /jest-each/29.4.3: - resolution: {integrity: sha512-1ElHNAnKcbJb/b+L+7j0/w7bDvljw4gTv1wL9fYOczeJrbTbkMGQ5iQPFJ3eFQH19VPTx1IyfePdqSpePKss7Q==} + /jest-each/29.5.0: + resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 chalk: 4.1.2 jest-get-type: 29.4.3 - jest-util: 29.4.3 - pretty-format: 29.4.3 + jest-util: 29.5.0 + pretty-format: 29.5.0 dev: true - /jest-environment-node/29.4.3: - resolution: {integrity: sha512-gAiEnSKF104fsGDXNkwk49jD/0N0Bqu2K9+aMQXA6avzsA9H3Fiv1PW2D+gzbOSR705bWd2wJZRFEFpV0tXISg==} + /jest-environment-node/29.5.0: + resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/fake-timers': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 18.14.1 - jest-mock: 29.4.3 - jest-util: 29.4.3 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 + jest-mock: 29.5.0 + jest-util: 29.5.0 dev: true /jest-get-type/29.2.0: @@ -8449,41 +8643,36 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-get-type/29.4.2: - resolution: {integrity: sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - /jest-get-type/29.4.3: resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-haste-map/29.4.3: - resolution: {integrity: sha512-eZIgAS8tvm5IZMtKlR8Y+feEOMfo2pSQkmNbufdbMzMSn9nitgGxF1waM/+LbryO3OkMcKS98SUb+j/cQxp/vQ==} + /jest-haste-map/29.5.0: + resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/graceful-fs': 4.1.6 - '@types/node': 18.14.1 + '@types/node': 18.15.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 jest-regex-util: 29.4.3 - jest-util: 29.4.3 - jest-worker: 29.4.3 + jest-util: 29.5.0 + jest-worker: 29.5.0 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 dev: true - /jest-leak-detector/29.4.3: - resolution: {integrity: sha512-9yw4VC1v2NspMMeV3daQ1yXPNxMgCzwq9BocCwYrRgXe4uaEJPAN0ZK37nFBhcy3cUwEVstFecFLaTHpF7NiGA==} + /jest-leak-detector/29.5.0: + resolution: {integrity: sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.4.3 - pretty-format: 29.4.3 + pretty-format: 29.5.0 dev: true /jest-matcher-utils/29.3.1: @@ -8491,19 +8680,19 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 - jest-diff: 29.4.2 - jest-get-type: 29.4.2 - pretty-format: 29.4.3 + jest-diff: 29.5.0 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 dev: true - /jest-matcher-utils/29.4.3: - resolution: {integrity: sha512-TTciiXEONycZ03h6R6pYiZlSkvYgT0l8aa49z/DLSGYjex4orMUcafuLXYyyEDWB1RKglq00jzwY00Ei7yFNVg==} + /jest-matcher-utils/29.5.0: + resolution: {integrity: sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 - jest-diff: 29.4.3 + jest-diff: 29.5.0 jest-get-type: 29.4.3 - pretty-format: 29.4.3 + pretty-format: 29.5.0 dev: true /jest-message-util/29.3.1: @@ -8511,41 +8700,41 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/code-frame': 7.18.6 - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.10 micromatch: 4.0.5 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 stack-utils: 2.0.6 dev: true - /jest-message-util/29.4.3: - resolution: {integrity: sha512-1Y8Zd4ZCN7o/QnWdMmT76If8LuDv23Z1DRovBj/vcSFNlGCJGoO8D1nJDw1AdyAGUk0myDLFGN5RbNeJyCRGCw==} + /jest-message-util/29.5.0: + resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/code-frame': 7.18.6 - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.10 micromatch: 4.0.5 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 stack-utils: 2.0.6 dev: true - /jest-mock/29.4.3: - resolution: {integrity: sha512-LjFgMg+xed9BdkPMyIJh+r3KeHt1klXPJYBULXVVAkbTaaKjPX1o1uVCAZADMEp/kOxGTwy/Ot8XbvgItOrHEg==} + /jest-mock/29.5.0: + resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 - '@types/node': 18.14.1 - jest-util: 29.4.3 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 + jest-util: 29.5.0 dev: true - /jest-pnp-resolver/1.2.3_jest-resolve@29.4.3: + /jest-pnp-resolver/1.2.3_jest-resolve@29.5.0: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} peerDependencies: @@ -8554,7 +8743,7 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 29.4.3 + jest-resolve: 29.5.0 dev: true /jest-regex-util/29.4.3: @@ -8562,92 +8751,92 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-resolve-dependencies/29.4.3: - resolution: {integrity: sha512-uvKMZAQ3nmXLH7O8WAOhS5l0iWyT3WmnJBdmIHiV5tBbdaDZ1wqtNX04FONGoaFvSOSHBJxnwAVnSn1WHdGVaw==} + /jest-resolve-dependencies/29.5.0: + resolution: {integrity: sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-regex-util: 29.4.3 - jest-snapshot: 29.4.3 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color dev: true - /jest-resolve/29.4.3: - resolution: {integrity: sha512-GPokE1tzguRyT7dkxBim4wSx6E45S3bOQ7ZdKEG+Qj0Oac9+6AwJPCk0TZh5Vu0xzeX4afpb+eDmgbmZFFwpOw==} + /jest-resolve/29.5.0: + resolution: {integrity: sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - jest-pnp-resolver: 1.2.3_jest-resolve@29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 + jest-haste-map: 29.5.0 + jest-pnp-resolver: 1.2.3_jest-resolve@29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 resolve: 1.22.1 resolve.exports: 2.0.0 slash: 3.0.0 dev: true - /jest-runner/29.4.3: - resolution: {integrity: sha512-GWPTEiGmtHZv1KKeWlTX9SIFuK19uLXlRQU43ceOQ2hIfA5yPEJC7AMkvFKpdCHx6pNEdOD+2+8zbniEi3v3gA==} + /jest-runner/29.5.0: + resolution: {integrity: sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.4.3 - '@jest/environment': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 18.14.1 + '@jest/console': 29.5.0 + '@jest/environment': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.10 jest-docblock: 29.4.3 - jest-environment-node: 29.4.3 - jest-haste-map: 29.4.3 - jest-leak-detector: 29.4.3 - jest-message-util: 29.4.3 - jest-resolve: 29.4.3 - jest-runtime: 29.4.3 - jest-util: 29.4.3 - jest-watcher: 29.4.3 - jest-worker: 29.4.3 + jest-environment-node: 29.5.0 + jest-haste-map: 29.5.0 + jest-leak-detector: 29.5.0 + jest-message-util: 29.5.0 + jest-resolve: 29.5.0 + jest-runtime: 29.5.0 + jest-util: 29.5.0 + jest-watcher: 29.5.0 + jest-worker: 29.5.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color dev: true - /jest-runtime/29.4.3: - resolution: {integrity: sha512-F5bHvxSH+LvLV24vVB3L8K467dt3y3dio6V3W89dUz9nzvTpqd/HcT9zfYKL2aZPvD63vQFgLvaUX/UpUhrP6Q==} + /jest-runtime/29.5.0: + resolution: {integrity: sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/fake-timers': 29.4.3 - '@jest/globals': 29.4.3 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/globals': 29.5.0 '@jest/source-map': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 18.14.1 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 glob: 7.2.3 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - jest-message-util: 29.4.3 - jest-mock: 29.4.3 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 + jest-resolve: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color dev: true - /jest-snapshot/29.4.3: - resolution: {integrity: sha512-NGlsqL0jLPDW91dz304QTM/SNO99lpcSYYAjNiX0Ou+sSGgkanKBcSjCfp/pqmiiO1nQaOyLp6XQddAzRcx3Xw==} + /jest-snapshot/29.5.0: + resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.20.12 @@ -8656,90 +8845,77 @@ packages: '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.20.12 '@babel/traverse': 7.20.12 '@babel/types': 7.20.7 - '@jest/expect-utils': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/expect-utils': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/babel__traverse': 7.18.3 '@types/prettier': 2.7.2 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.12 chalk: 4.1.2 - expect: 29.4.3 + expect: 29.5.0 graceful-fs: 4.2.10 - jest-diff: 29.4.3 + jest-diff: 29.5.0 jest-get-type: 29.4.3 - jest-haste-map: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-util: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 natural-compare: 1.4.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 semver: 7.3.8 transitivePeerDependencies: - supports-color dev: true - /jest-util/29.4.2: - resolution: {integrity: sha512-wKnm6XpJgzMUSRFB7YF48CuwdzuDIHenVuoIb1PLuJ6F+uErZsuDkU+EiExkChf6473XcawBrSfDSnXl+/YG4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.4.3 - '@types/node': 18.14.1 - chalk: 4.1.2 - ci-info: 3.7.1 - graceful-fs: 4.2.10 - picomatch: 2.3.1 - dev: true - - /jest-util/29.4.3: - resolution: {integrity: sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q==} + /jest-util/29.5.0: + resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 - '@types/node': 18.14.1 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 chalk: 4.1.2 ci-info: 3.7.1 graceful-fs: 4.2.10 picomatch: 2.3.1 dev: true - /jest-validate/29.4.3: - resolution: {integrity: sha512-J3u5v7aPQoXPzaar6GndAVhdQcZr/3osWSgTeKg5v574I9ybX/dTyH0AJFb5XgXIB7faVhf+rS7t4p3lL9qFaw==} + /jest-validate/29.5.0: + resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 camelcase: 6.3.0 chalk: 4.1.2 jest-get-type: 29.4.3 leven: 3.1.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 dev: true - /jest-watcher/29.4.3: - resolution: {integrity: sha512-zwlXH3DN3iksoIZNk73etl1HzKyi5FuQdYLnkQKm5BW4n8HpoG59xSwpVdFrnh60iRRaRBGw0gcymIxjJENPcA==} + /jest-watcher/29.5.0: + resolution: {integrity: sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 - '@types/node': 18.14.1 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.15.0 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 29.4.3 + jest-util: 29.5.0 string-length: 4.0.2 dev: true - /jest-worker/29.4.3: - resolution: {integrity: sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA==} + /jest-worker/29.5.0: + resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 18.14.1 - jest-util: 29.4.3 + '@types/node': 18.15.0 + jest-util: 29.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest/29.4.3_@types+node@18.14.1: - resolution: {integrity: sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA==} + /jest/29.5.0_@types+node@18.15.0: + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -8748,10 +8924,10 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.4.3 - '@jest/types': 29.4.3 + '@jest/core': 29.5.0 + '@jest/types': 29.5.0 import-local: 3.1.0 - jest-cli: 29.4.3_@types+node@18.14.1 + jest-cli: 29.5.0_@types+node@18.15.0 transitivePeerDependencies: - '@types/node' - supports-color @@ -8785,6 +8961,17 @@ packages: resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==} dev: false + /js-beautify/1.14.6: + resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + config-chain: 1.1.13 + editorconfig: 0.15.3 + glob: 8.1.0 + nopt: 6.0.0 + dev: true + /js-sdsl/4.2.0: resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} dev: true @@ -8828,7 +9015,6 @@ packages: /jschardet/3.0.0: resolution: {integrity: sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==} engines: {node: '>=0.1.90'} - dev: false /jsdom/21.1.0: resolution: {integrity: sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg==} @@ -8874,11 +9060,11 @@ packages: /jsesc/2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} + hasBin: true dev: true /json-buffer/3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: false /json-parse-even-better-errors/2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -9006,7 +9192,6 @@ packages: resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==} dependencies: json-buffer: 3.0.1 - dev: false /kind-of/3.2.2: resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} @@ -9179,6 +9364,11 @@ packages: resolution: {integrity: sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA==} dev: false + /local-pkg/0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + dev: true + /locate-path/5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -9254,6 +9444,12 @@ packages: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} dev: false + /loupe/2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + dependencies: + get-func-name: 2.0.0 + dev: true + /lowercase-keys/2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} @@ -9262,14 +9458,12 @@ packages: /lowercase-keys/3.0.0: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false /lru-cache/4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} dependencies: pseudomap: 1.0.2 yallist: 2.1.2 - dev: false /lru-cache/5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -9292,6 +9486,11 @@ packages: resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==} engines: {node: '>=12'} + /lz-string/1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + /magic-string/0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: @@ -9313,7 +9512,7 @@ packages: dependencies: agentkeepalive: 4.2.1 cacache: 16.1.3 - http-cache-semantics: 4.1.0 + http-cache-semantics: 4.1.1 http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 is-lambda: 1.0.1 @@ -9454,12 +9653,10 @@ packages: /mimic-response/3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - dev: false /mimic-response/4.0.0: resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false /minimalistic-assert/1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -9475,7 +9672,6 @@ packages: engines: {node: '>=10'} dependencies: brace-expansion: 2.0.1 - dev: false /minimatch/6.2.0: resolution: {integrity: sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==} @@ -9594,6 +9790,15 @@ packages: engines: {node: '>=10'} dev: false + /mlly/1.1.1: + resolution: {integrity: sha512-Jnlh4W/aI4GySPo6+DyTN17Q75KKbLTyFK8BrGhjNP4rxuUjbRWhE6gHg3bs33URWAF44FRm7gdQA348i3XxRw==} + dependencies: + acorn: 8.8.2 + pathe: 1.1.0 + pkg-types: 1.0.2 + ufo: 1.1.1 + dev: true + /mnemonist/0.39.5: resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==} dependencies: @@ -9750,7 +9955,6 @@ packages: /netmask/2.0.2: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} - dev: false /next-tick/1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -9786,7 +9990,6 @@ packages: optional: true dependencies: whatwg-url: 5.0.0 - dev: false /node-fetch/3.3.0: resolution: {integrity: sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==} @@ -9851,9 +10054,9 @@ packages: /nopt/6.0.0: resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true dependencies: abbrev: 1.1.1 - dev: false /normalize-package-data/2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -9898,7 +10101,6 @@ packages: /normalize-url/8.0.0: resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} engines: {node: '>=14.16'} - dev: false /now-and-later/2.0.1: resolution: {integrity: sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==} @@ -10004,6 +10206,14 @@ packages: resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} dev: true + /object-is/1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + dev: true + /object-keys/1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -10170,7 +10380,6 @@ packages: /p-cancelable/3.0.0: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} - dev: false /p-finally/1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} @@ -10189,6 +10398,13 @@ packages: dependencies: yocto-queue: 0.1.0 + /p-limit/4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate/4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -10300,7 +10516,6 @@ packages: dependencies: domhandler: 5.0.3 parse5: 7.1.2 - dev: false /parse5/5.1.1: resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} @@ -10385,6 +10600,14 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + /pathe/1.1.0: + resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + dev: true + + /pathval/1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /pause-stream/0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} dependencies: @@ -10411,12 +10634,12 @@ packages: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - /pg-pool/3.5.2_pg@8.9.0: - resolution: {integrity: sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==} + /pg-pool/3.6.0_pg@8.10.0: + resolution: {integrity: sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==} peerDependencies: pg: '>=8.0' dependencies: - pg: 8.9.0 + pg: 8.10.0 dev: false /pg-protocol/1.5.0: @@ -10437,8 +10660,8 @@ packages: postgres-date: 1.0.7 postgres-interval: 1.2.0 - /pg/8.9.0: - resolution: {integrity: sha512-ZJM+qkEbtOHRuXjmvBtOgNOXOtLSbxiMiUVMgE4rV6Zwocy03RicCVvDXgx8l4Biwo8/qORUnEqn2fdQzV7KCg==} + /pg/8.10.0: + resolution: {integrity: sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==} engines: {node: '>= 8.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -10449,7 +10672,7 @@ packages: buffer-writer: 2.0.0 packet-reader: 1.0.0 pg-connection-string: 2.5.0 - pg-pool: 3.5.2_pg@8.9.0 + pg-pool: 3.6.0_pg@8.10.0 pg-protocol: 1.6.0 pg-types: 2.2.0 pgpass: 1.0.5 @@ -10502,6 +10725,7 @@ packages: /pino/8.8.0: resolution: {integrity: sha512-cF8iGYeu2ODg2gIwgAHcPrtR63ILJz3f7gkogaHC/TXVVXxZgInmNYiIpDYEwgEkxZti2Se6P2W2DxlBIZe6eQ==} + hasBin: true dependencies: atomic-sleep: 1.0.0 fast-redact: 3.1.2 @@ -10528,6 +10752,14 @@ packages: find-up: 4.1.0 dev: true + /pkg-types/1.0.2: + resolution: {integrity: sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.1.1 + pathe: 1.1.0 + dev: true + /plimit-lit/1.5.0: resolution: {integrity: sha512-Eb/MqCb1Iv/ok4m1FqIXqvUKPISufcjZ605hl3KM/n8GaX8zfhtgdLwZU3vKjuHGh2O9Rjog/bHTq8ofIShdng==} dependencies: @@ -10847,6 +11079,15 @@ packages: engines: {node: '>=6'} dev: true + /pretty-format/27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + /pretty-format/29.3.1: resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10856,8 +11097,8 @@ packages: react-is: 18.2.0 dev: true - /pretty-format/29.4.3: - resolution: {integrity: sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA==} + /pretty-format/29.5.0: + resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.4.3 @@ -10882,7 +11123,6 @@ packages: ipaddr.js: 2.0.1 is-ip: 3.1.0 netmask: 2.0.2 - dev: false /private-ip/3.0.0: resolution: {integrity: sha512-HkMBs4nMtrP+cvcw0bDi2BAZIGgiKI4Zq8Oc+dMqNBpHS8iGL4+WO/pRtc8Bwnv9rjnV0QwMDwEBymFtqv7Kww==} @@ -10957,6 +11197,10 @@ packages: sisteransi: 1.0.5 dev: true + /proto-list/1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: true + /proxy-addr/2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -10972,13 +11216,13 @@ packages: /ps-tree/1.2.0: resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} engines: {node: '>= 0.10'} + hasBin: true dependencies: event-stream: 3.3.4 dev: true /pseudomap/1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - dev: false /psl/1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} @@ -11103,6 +11347,10 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + /pure-rand/6.0.0: + resolution: {integrity: sha512-rLSBxJjP+4DQOgcJAx6RZHT2he2pkhQdSnofG5VWyVl6GRq/K02ISOuOLcsMOrtKDIJb8JN2zm3FFzWNbezdPw==} + dev: true + /pureimage/0.3.17: resolution: {integrity: sha512-JV4hfYF1BXxDwbSR8hjhVEhVTxwmAXos8uIXQ7Bw2eWrUEpLDJnQoQ8WLlWAO4TMGJ7mp9n6gvLKJ6MSaGUkXQ==} engines: {node: '>=0.8'} @@ -11175,7 +11423,6 @@ packages: /quick-lru/5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - dev: false /random-seed/0.3.0: resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==} @@ -11224,6 +11471,10 @@ packages: - supports-color dev: false + /react-is/17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + /react-is/18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true @@ -11376,7 +11627,6 @@ packages: /regenerator-runtime/0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - dev: false /regex-not/1.0.2: resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} @@ -11516,7 +11766,6 @@ packages: /resolve-alpn/1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - dev: false /resolve-cwd/3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} @@ -11577,7 +11826,6 @@ packages: engines: {node: '>=14.16'} dependencies: lowercase-keys: 3.0.0 - dev: false /restore-cursor/3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} @@ -11617,6 +11865,7 @@ packages: /rimraf/3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true dependencies: glob: 7.2.3 @@ -11627,13 +11876,12 @@ packages: seedrandom: 2.4.2 dev: false - /rollup/3.17.3: - resolution: {integrity: sha512-p5LaCXiiOL/wrOkj8djsIDFmyU9ysUxcyW+EKRLHb6TKldJzXpImjcRSR+vgo09DBdofGcOoLOsRyxxG2n5/qQ==} + /rollup/3.19.0: + resolution: {integrity: sha512-xZzJZlH9Ca6cosfdNGPwl2z7Pby8dTi9TrYLPeg6/j7aUoDOhBd706tCUFvbiBj45h/cS7z/a4gS8xd5Yg0jBw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: fsevents: 2.3.2 - dev: false /rss-parser/3.12.0: resolution: {integrity: sha512-aqD3E8iavcCdkhVxNDIdg1nkBI17jgqF+9OqPS1orwNaOgySdpvq6B+DoONLhzjzwV8mWg37sb60e4bmLK117A==} @@ -11671,7 +11919,7 @@ packages: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.3 + get-intrinsic: 1.2.0 is-regex: 1.1.4 dev: true @@ -11713,7 +11961,6 @@ packages: chokidar: 3.5.3 immutable: 4.2.2 source-map-js: 1.0.2 - dev: false /sax/1.2.1: resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} @@ -11764,10 +12011,10 @@ packages: /semver/5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true - dev: false /semver/6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true /semver/7.3.8: resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} @@ -11853,6 +12100,14 @@ packages: object-inspect: 1.12.2 dev: true + /siginfo/2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /sigmund/1.0.1: + resolution: {integrity: sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==} + dev: true + /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -11905,6 +12160,14 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true + /slice-ansi/5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + dev: true + /smart-buffer/4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -12118,12 +12381,17 @@ packages: escape-string-regexp: 2.0.0 dev: true + /stackback/0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + /standard-as-callback/2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - /start-server-and-test/1.15.4: - resolution: {integrity: sha512-ucQtp5+UCr0m4aHlY+aEV2JSYNTiMZKdSKK/bsIr6AlmwAWDYDnV7uGlWWEtWa7T4XvRI5cPYcPcQgeLqpz+Tg==} + /start-server-and-test/2.0.0: + resolution: {integrity: sha512-UqKLw0mJbfrsG1jcRLTUlvuRi9sjNuUiDOLI42r7R5fA9dsFoywAy9DoLXNYys9B886E4RCKb+qM1Gzu96h7DQ==} engines: {node: '>=6'} + hasBin: true dependencies: arg: 5.0.2 bluebird: 3.7.2 @@ -12150,6 +12418,17 @@ packages: engines: {node: '>= 0.8'} dev: false + /std-env/3.3.2: + resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} + dev: true + + /stop-iteration-iterator/1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.5 + dev: true + /stream-combiner/0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} dependencies: @@ -12215,6 +12494,15 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width/5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.0.1 + dev: true + /string.prototype.trimend/1.0.6: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: @@ -12266,6 +12554,13 @@ packages: dependencies: ansi-regex: 5.0.1 + /strip-ansi/7.0.1: + resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + /strip-bom/2.0.0: resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==} engines: {node: '>=0.10.0'} @@ -12306,6 +12601,12 @@ packages: engines: {node: '>=8'} dev: true + /strip-literal/1.0.1: + resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + dependencies: + acorn: 8.8.2 + dev: true + /strip-outer/2.0.0: resolution: {integrity: sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12383,8 +12684,8 @@ packages: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: false - /systeminformation/5.17.10: - resolution: {integrity: sha512-FUm264baeDpruTw4P50BRRmYHD39D3jkOQ0VpNIkp8CdNejQbsp4Me18jacGBc/mWSVxKdQw4wSHmcL7ERxrNg==} + /systeminformation/5.17.12: + resolution: {integrity: sha512-I3pfMW2vue53u+X08BNxaJieaHkRoMMKjWetY9lbYJeWFaeWPO6P4FkNc4XOCX8F9vbQ0HqQ25RJoz3U/B7liw==} engines: {node: '>=8.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true @@ -12445,7 +12746,7 @@ packages: engines: {node: '>=10'} dependencies: '@jridgewell/source-map': 0.3.2 - acorn: 8.8.1 + acorn: 8.8.2 commander: 2.20.3 source-map-support: 0.5.21 dev: false @@ -12495,8 +12796,8 @@ packages: real-require: 0.2.0 dev: false - /three/0.150.0: - resolution: {integrity: sha512-12oqqBZom9fb5HtX3rD8qPVnamojuiN5Os7r0x8s3HQ+WHRwnEyzl2XU3aEKocsDkG++rkE9+HWzx77O59NXtw==} + /three/0.150.1: + resolution: {integrity: sha512-5C1MqKUWaHYo13BX0Q64qcdwImgnnjSOFgBscOzAo8MYCzEtqfQqorEKMcajnA3FHy1yVlIe9AmaMQ0OQracNA==} dev: false /throttle-debounce/5.0.0: @@ -12545,10 +12846,24 @@ packages: engines: {node: '>=6'} dev: false + /tinybench/2.4.0: + resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==} + dev: true + /tinycolor2/1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} dev: false + /tinypool/0.3.1: + resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy/1.1.1: + resolution: {integrity: sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==} + engines: {node: '>=14.0.0'} + dev: true + /tmp/0.2.1: resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} engines: {node: '>=8.17.0'} @@ -12649,7 +12964,6 @@ packages: /tr46/0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false /tr46/3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} @@ -12660,7 +12974,6 @@ packages: /trace-redirect/1.0.6: resolution: {integrity: sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==} - dev: false /traverse/0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} @@ -12673,8 +12986,9 @@ packages: escape-string-regexp: 5.0.0 dev: false - /tsc-alias/1.8.2: - resolution: {integrity: sha512-ukBkcNekOgwtnSWYLD5QsMX3yQWg7JviAs8zg3qJGgu4LGtY3tsV4G6vnqvOXIDkbC+XL9vbhObWSpRA5/6wbg==} + /tsc-alias/1.8.3: + resolution: {integrity: sha512-/9JARcmXBrEqSuLjdSOqxY7/xI/AnvmBi4CU9/Ba2oX6Oq8vnd0OGSQTk+PIwqWJ5ZxskV0X/x15yzxCNTHU+g==} + hasBin: true dependencies: chokidar: 3.5.3 commander: 9.5.0 @@ -12781,7 +13095,7 @@ packages: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} dev: false - /typeorm/0.3.11_ioredis@4.28.5+pg@8.9.0: + /typeorm/0.3.11_ioredis@4.28.5+pg@8.10.0: resolution: {integrity: sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==} engines: {node: '>= 12.9.0'} hasBin: true @@ -12851,7 +13165,7 @@ packages: ioredis: 4.28.5 js-yaml: 4.1.0 mkdirp: 1.0.4 - pg: 8.9.0 + pg: 8.10.0 reflect-metadata: 0.1.13 sha.js: 2.4.11 tslib: 2.5.0 @@ -12866,6 +13180,10 @@ packages: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} + /ufo/1.1.1: + resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} + dev: true + /uid/2.0.1: resolution: {integrity: sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A==} engines: {node: '>=8'} @@ -13194,7 +13512,28 @@ packages: replace-ext: 1.0.1 dev: false - /vite/4.1.4_435aevtanapkguv7m72cl6trbi: + /vite-node/0.29.2_6e4omgvd5jf4hig7wpb5tmdc3q: + resolution: {integrity: sha512-5oe1z6wzI3gkvc4yOBbDBbgpiWiApvuN4P55E8OI131JGrSuo4X3SOZrNmZYo4R8Zkze/dhi572blX0zc+6SdA==} + engines: {node: '>=v14.16.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.1.1 + pathe: 1.1.0 + picocolors: 1.0.0 + vite: 4.1.4_6e4omgvd5jf4hig7wpb5tmdc3q + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite/4.1.4_6e4omgvd5jf4hig7wpb5tmdc3q: resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -13219,15 +13558,82 @@ packages: terser: optional: true dependencies: - '@types/node': 18.14.1 + '@types/node': 18.15.0 esbuild: 0.16.17 postcss: 8.4.21 resolve: 1.22.1 - rollup: 3.17.3 + rollup: 3.19.0 sass: 1.58.3 optionalDependencies: fsevents: 2.3.2 - dev: false + + /vitest-fetch-mock/0.2.2_vitest@0.29.2: + resolution: {integrity: sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==} + engines: {node: '>=14.14.0'} + peerDependencies: + vitest: '>=0.16.0' + dependencies: + cross-fetch: 3.1.5 + vitest: 0.29.2_zcjcryjt4bqcdu7ggonulipgea + transitivePeerDependencies: + - encoding + dev: true + + /vitest/0.29.2_zcjcryjt4bqcdu7ggonulipgea: + resolution: {integrity: sha512-ydK9IGbAvoY8wkg29DQ4ivcVviCaUi3ivuPKfZEVddMTenFHUfB8EEDXQV8+RasEk1ACFLgMUqAaDuQ/Nk+mQA==} + engines: {node: '>=v14.16.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/chai': 4.3.4 + '@types/chai-subset': 1.3.3 + '@types/node': 18.15.0 + '@vitest/expect': 0.29.2 + '@vitest/runner': 0.29.2 + '@vitest/spy': 0.29.2 + '@vitest/utils': 0.29.2 + acorn: 8.8.1 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.7 + debug: 4.3.4 + happy-dom: 8.9.0 + local-pkg: 0.4.3 + pathe: 1.1.0 + picocolors: 1.0.0 + source-map: 0.6.1 + std-env: 3.3.2 + strip-literal: 1.0.1 + tinybench: 2.4.0 + tinypool: 0.3.1 + tinyspy: 1.1.1 + vite: 4.1.4_6e4omgvd5jf4hig7wpb5tmdc3q + vite-node: 0.29.2_6e4omgvd5jf4hig7wpb5tmdc3q + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true /void-elements/3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} @@ -13301,7 +13707,6 @@ packages: '@vue/runtime-dom': 3.2.47 '@vue/server-renderer': 3.2.47_vue@3.2.47 '@vue/shared': 3.2.47 - dev: false /vuedraggable/4.1.0_vue@3.2.47: resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} @@ -13322,6 +13727,7 @@ packages: /wait-on/7.0.1_debug@4.3.4: resolution: {integrity: sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==} engines: {node: '>=12.0.0'} + hasBin: true dependencies: axios: 0.27.2_debug@4.3.4 joi: 17.7.0 @@ -13358,12 +13764,10 @@ packages: /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false /webidl-conversions/7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - dev: false /websocket/1.0.34: resolution: {integrity: sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==} @@ -13384,12 +13788,10 @@ packages: engines: {node: '>=12'} dependencies: iconv-lite: 0.6.3 - dev: false /whatwg-mimetype/3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} - dev: false /whatwg-url/11.0.0: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} @@ -13404,7 +13806,6 @@ packages: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - dev: false /whet.extend/0.9.9: resolution: {integrity: sha512-mmIPAft2vTgEILgPeZFqE/wWh24SEsR/k+N9fJ3Jxrz44iDFy9aemCxdksfURSHYFCLmvs/d/7Iso5XjPpNfrA==} @@ -13421,6 +13822,15 @@ packages: is-symbol: 1.0.4 dev: true + /which-collection/1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + dependencies: + is-map: 2.0.2 + is-set: 2.0.2 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + dev: true + /which-module/1.0.0: resolution: {integrity: sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==} dev: false @@ -13439,10 +13849,10 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 is-typed-array: 1.1.10 - dev: false /which/1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true dependencies: isexe: 2.0.0 dev: false @@ -13450,9 +13860,19 @@ packages: /which/2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} + hasBin: true dependencies: isexe: 2.0.0 + /why-is-node-running/2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /wide-align/1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} dependencies: @@ -13587,7 +14007,6 @@ packages: /yallist/2.1.2: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} - dev: false /yallist/3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -13606,7 +14025,6 @@ packages: /yargs-parser/20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} - dev: false /yargs-parser/21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} @@ -13647,7 +14065,6 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 20.2.9 - dev: false /yargs/17.6.2: resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==} @@ -13690,6 +14107,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + /yocto-queue/1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true + /zip-stream/4.1.0: resolution: {integrity: sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==} engines: {node: '>= 10'} @@ -13715,20 +14137,19 @@ packages: sharp: 0.31.3 dev: false - github.com/misskey-dev/summaly/51f3870e1ff5e0b22102e804112b10cb72f3c494: - resolution: {tarball: https://codeload.github.com/misskey-dev/summaly/tar.gz/51f3870e1ff5e0b22102e804112b10cb72f3c494} + github.com/misskey-dev/summaly/1bab7afee616429b8bbf7a7cbcbb8ebcef66d992: + resolution: {tarball: https://codeload.github.com/misskey-dev/summaly/tar.gz/1bab7afee616429b8bbf7a7cbcbb8ebcef66d992} name: summaly - version: 3.0.4 + version: 4.0.1 dependencies: cheerio: 1.0.0-rc.12 escape-regexp: 0.0.1 - got: 12.5.3 + got: 12.6.0 html-entities: 2.3.2 iconv-lite: 0.6.3 jschardet: 3.0.0 private-ip: 2.3.3 trace-redirect: 1.0.6 - dev: false github.com/sampotts/plyr/d434c9af16e641400aaee93188594208d88f2658: resolution: {tarball: https://codeload.github.com/sampotts/plyr/tar.gz/d434c9af16e641400aaee93188594208d88f2658} |