Merge branch 'develop' into feat-1714

This commit is contained in:
かっこかり
2024-07-15 10:11:15 +09:00
committed by GitHub
85 changed files with 1222 additions and 831 deletions

View File

@@ -1,5 +1,11 @@
# misskey settings
# MISSKEY_URL=https://example.tld/
# db settings # db settings
POSTGRES_PASSWORD=example-misskey-pass POSTGRES_PASSWORD=example-misskey-pass
# DATABASE_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_USER=example-misskey-user POSTGRES_USER=example-misskey-user
# DATABASE_USER=${POSTGRES_USER}
POSTGRES_DB=misskey POSTGRES_DB=misskey
# DATABASE_DB=${POSTGRES_DB}
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"

View File

@@ -6,6 +6,7 @@
#───┘ URL └───────────────────────────────────────────────────── #───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user. # Final accessible URL seen by a user.
# You can set url from an environment variable instead.
url: https://example.tld/ url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
@@ -38,9 +39,11 @@ db:
port: 5432 port: 5432
# Database name # Database name
# You can set db from an environment variable instead.
db: misskey db: misskey
# Auth # Auth
# You can set user and pass from environment variables instead.
user: example-misskey-user user: example-misskey-user
pass: example-misskey-pass pass: example-misskey-pass

View File

@@ -4,10 +4,11 @@ on:
push: push:
paths: paths:
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/api-misskey-js.yml
pull_request: pull_request:
paths: paths:
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/api-misskey-js.yml
jobs: jobs:
report: report:
@@ -20,7 +21,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -14,7 +14,7 @@ jobs:
- name: Checkout head - name: Checkout head
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -28,7 +28,7 @@ jobs:
- name: setup node - name: setup node
id: setup-node id: setup-node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: pnpm cache: pnpm

View File

@@ -6,12 +6,13 @@ on:
paths: paths:
- packages/misskey-js/package.json - packages/misskey-js/package.json
- package.json - package.json
- .github/workflows/check-misskey-js-version.yml
pull_request: pull_request:
branches: [ develop ] branches: [ develop ]
paths: paths:
- packages/misskey-js/package.json - packages/misskey-js/package.json
- package.json - package.json
- .github/workflows/check-misskey-js-version.yml
jobs: jobs:
check-version: check-version:
# ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する # ルートの package.json と packages/misskey-js/package.json のバージョンが一致しているかを確認する

View File

@@ -9,7 +9,7 @@ on:
paths: paths:
- packages/backend/** - packages/backend/**
- .github/workflows/get-api-diff.yml - .github/workflows/get-api-diff.yml
- .github/workflows/get-api-diff.yml
jobs: jobs:
get-from-misskey: get-from-misskey:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -34,7 +34,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -11,6 +11,7 @@ on:
- packages/sw/** - packages/sw/**
- packages/misskey-js/** - packages/misskey-js/**
- packages/shared/eslint.config.js - packages/shared/eslint.config.js
- .github/workflows/lint.yml
pull_request: pull_request:
paths: paths:
- packages/backend/** - packages/backend/**
@@ -18,7 +19,7 @@ on:
- packages/sw/** - packages/sw/**
- packages/misskey-js/** - packages/misskey-js/**
- packages/shared/eslint.config.js - packages/shared/eslint.config.js
- .github/workflows/lint.yml
jobs: jobs:
pnpm_install: pnpm_install:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -28,7 +29,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@@ -39,6 +40,8 @@ jobs:
needs: [pnpm_install] needs: [pnpm_install]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
env:
eslint-cache-version: v1
strategy: strategy:
matrix: matrix:
workspace: workspace:
@@ -52,13 +55,20 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
- run: corepack enable - run: corepack enable
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- run: pnpm --filter ${{ matrix.workspace }} run eslint - name: Restore eslint cache
uses: actions/cache@v4.0.2
with:
path: node_modules/.cache/eslint
key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-
- run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content
typecheck: typecheck:
needs: [pnpm_install] needs: [pnpm_install]
@@ -75,7 +85,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -4,10 +4,11 @@ on:
push: push:
paths: paths:
- locales/** - locales/**
- .github/workflows/locale.yml
pull_request: pull_request:
paths: paths:
- locales/** - locales/**
- .github/workflows/locale.yml
jobs: jobs:
locale_verify: locale_verify:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -18,7 +19,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.0.2 - uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -26,7 +26,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -6,7 +6,7 @@ on:
- develop - develop
paths: paths:
- 'CHANGELOG.md' - 'CHANGELOG.md'
# - .github/workflows/release-edit-with-push.yml
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -36,7 +36,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -9,12 +9,13 @@ on:
- packages/backend/** - packages/backend/**
# for permissions # for permissions
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/test-backend.yml
pull_request: pull_request:
paths: paths:
- packages/backend/** - packages/backend/**
# for permissions # for permissions
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/test-backend.yml
jobs: jobs:
unit: unit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -45,7 +46,7 @@ jobs:
- name: Install FFmpeg - name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3 uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@@ -92,7 +93,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -11,7 +11,7 @@ on:
- packages/misskey-js/** - packages/misskey-js/**
# for e2e # for e2e
- packages/backend/** - packages/backend/**
- .github/workflows/test-frontend.yml
pull_request: pull_request:
paths: paths:
- packages/frontend/** - packages/frontend/**
@@ -19,7 +19,7 @@ on:
- packages/misskey-js/** - packages/misskey-js/**
# for e2e # for e2e
- packages/backend/** - packages/backend/**
- .github/workflows/test-frontend.yml
jobs: jobs:
vitest: vitest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -35,7 +35,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@@ -90,7 +90,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -8,11 +8,12 @@ on:
branches: [ develop ] branches: [ develop ]
paths: paths:
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/test-misskey-js.yml
pull_request: pull_request:
branches: [ develop ] branches: [ develop ]
paths: paths:
- packages/misskey-js/** - packages/misskey-js/**
- .github/workflows/test-misskey-js.yml
jobs: jobs:
test: test:
@@ -30,7 +31,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -25,7 +25,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -7,10 +7,11 @@ on:
- develop - develop
paths: paths:
- packages/backend/** - packages/backend/**
- .github/workflows/validate-api-json.yml
pull_request: pull_request:
paths: paths:
- packages/backend/** - packages/backend/**
- .github/workflows/validate-api-json.yml
jobs: jobs:
validate-api-json: validate-api-json:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -26,7 +27,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -5,17 +5,23 @@
### General ### General
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705 - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題 - Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正 - Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
### Client ### Client
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能 - Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
- 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です - 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善 - Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
- Enhance: 非ログイン時のハイライトTLのデザインを改善 - Enhance: 非ログイン時のハイライトTLのデザインを改善
- Enhance: フロントエンドのアクセシビリティ改善 - Enhance: フロントエンドのアクセシビリティ改善
(Based on https://github.com/taiyme/misskey/pull/226) (Based on https://github.com/taiyme/misskey/pull/226)
- Enhance: サーバー情報ページ・お問い合わせページを改善
(Cherry-picked from https://github.com/taiyme/misskey/pull/238)
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 - Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正 - Fix: リバーシの対局を正しく共有できないことがある問題を修正
@@ -33,6 +39,7 @@
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに - Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに - Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに - Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
- Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 - Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006) - Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) - Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
@@ -48,6 +55,9 @@
2. フォロー中かつ非アクティブなユーザ 2. フォロー中かつ非アクティブなユーザ
3. フォローしていないアクティブなユーザ 3. フォローしていないアクティブなユーザ
4. フォローしていない非アクティブなユーザ 4. フォローしていない非アクティブなユーザ
- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
### Misskey.js ### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)

View File

@@ -17,6 +17,8 @@ services:
networks: networks:
- internal_network - internal_network
- external_network - external_network
# env_file:
# - .config/docker.env
volumes: volumes:
- ./files:/misskey/files - ./files:/misskey/files
- ./.config:/misskey/.config:ro - ./.config:/misskey/.config:ro

30
locales/index.d.ts vendored
View File

@@ -736,6 +736,22 @@ export interface Locale extends ILocale {
* リモートで表示 * リモートで表示
*/ */
"showOnRemote": string; "showOnRemote": string;
/**
* リモートで続行
*/
"continueOnRemote": string;
/**
* Misskey Hubからサーバーを選択
*/
"chooseServerOnMisskeyHub": string;
/**
* サーバーのドメインを直接指定
*/
"specifyServerHost": string;
/**
* ドメインを入力してください
*/
"inputHostName": string;
/** /**
* 全般 * 全般
*/ */
@@ -1921,9 +1937,13 @@ export interface Locale extends ILocale {
*/ */
"onlyOneFileCanBeAttached": string; "onlyOneFileCanBeAttached": string;
/** /**
* 続行する前に、サインアップまたはサインインが必要です * 続行する前に、登録またはログインが必要です
*/ */
"signinRequired": string; "signinRequired": string;
/**
* 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
*/
"signinOrContinueOnRemote": string;
/** /**
* 招待 * 招待
*/ */
@@ -4984,6 +5004,10 @@ export interface Locale extends ILocale {
* お問い合わせ * お問い合わせ
*/ */
"inquiry": string; "inquiry": string;
/**
* もう一度お試しください。
*/
"tryAgain": string;
/** /**
* {x}から * {x}から
*/ */
@@ -6606,6 +6630,10 @@ export interface Locale extends ILocale {
* ファイルにNSFWを常に付与 * ファイルにNSFWを常に付与
*/ */
"alwaysMarkNsfw": string; "alwaysMarkNsfw": string;
/**
* アイコンとバナーの更新を許可
*/
"canUpdateBioMedia": string;
/** /**
* ノートのピン留めの最大数 * ノートのピン留めの最大数
*/ */

View File

@@ -180,6 +180,10 @@ addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新" reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗しました" loginFailed: "ログインに失敗しました"
showOnRemote: "リモートで表示" showOnRemote: "リモートで表示"
continueOnRemote: "リモートで続行"
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
specifyServerHost: "サーバーのドメインを直接指定"
inputHostName: "ドメインを入力してください"
general: "全般" general: "全般"
wallpaper: "壁紙" wallpaper: "壁紙"
setWallpaper: "壁紙を設定" setWallpaper: "壁紙を設定"
@@ -476,7 +480,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
noMessagesYet: "まだチャットはありません" noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります" newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです" onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
signinRequired: "続行する前に、サインアップまたはサインインが必要です" signinRequired: "続行する前に、登録またはログインが必要です"
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
invitations: "招待" invitations: "招待"
invitationCode: "招待コード" invitationCode: "招待コード"
checking: "確認しています" checking: "確認しています"
@@ -1242,6 +1247,7 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
noDescription: "説明文はありません" noDescription: "説明文はありません"
alwaysConfirmFollow: "フォローの際常に確認する" alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "お問い合わせ" inquiry: "お問い合わせ"
tryAgain: "もう一度お試しください。"
fromX: "{x}から" fromX: "{x}から"
copyEmbedCode: "埋め込みコードをコピー" copyEmbedCode: "埋め込みコードをコピー"
noteOfThisUser: "このユーザーのノート一覧" noteOfThisUser: "このユーザーのノート一覧"
@@ -1708,6 +1714,7 @@ _role:
canManageAvatarDecorations: "アバターデコレーションの管理" canManageAvatarDecorations: "アバターデコレーションの管理"
driveCapacity: "ドライブ容量" driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与" alwaysMarkNsfw: "ファイルにNSFWを常に付与"
canUpdateBioMedia: "アイコンとバナーの更新を許可"
pinMax: "ノートのピン留めの最大数" pinMax: "ノートのピン留めの最大数"
antennaMax: "アンテナの作成可能数" antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数" wordMuteMax: "ワードミュートの最大文字数"

View File

@@ -23,7 +23,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
* 設定ファイルの型 * 設定ファイルの型
*/ */
type Source = { type Source = {
url: string; url?: string;
port?: number; port?: number;
socket?: string; socket?: string;
chmodSocket?: string; chmodSocket?: string;
@@ -31,9 +31,9 @@ type Source = {
db: { db: {
host: string; host: string;
port: number; port: number;
db: string; db?: string;
user: string; user?: string;
pass: string; pass?: string;
disableCache?: boolean; disableCache?: boolean;
extra?: { [x: string]: string }; extra?: { [x: string]: string };
}; };
@@ -203,13 +203,17 @@ export function loadConfig(): Config {
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' }, 'src/_embed_boot_.ts': { file: 'src/_embed_boot_.ts' } }; : { 'src/_boot_.ts': { file: 'src/_boot_.ts' }, 'src/_embed_boot_.ts': { file: 'src/_embed_boot_.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const url = tryCreateUrl(config.url); const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
const version = meta.version; const version = meta.version;
const host = url.host; const host = url.host;
const hostname = url.hostname; const hostname = url.hostname;
const scheme = url.protocol.replace(/:$/, ''); const scheme = url.protocol.replace(/:$/, '');
const wsScheme = scheme.replace('http', 'ws'); const wsScheme = scheme.replace('http', 'ws');
const dbDb = config.db.db ?? process.env.DATABASE_DB ?? '';
const dbUser = config.db.user ?? process.env.DATABASE_USER ?? '';
const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? '';
const externalMediaProxy = config.mediaProxy ? const externalMediaProxy = config.mediaProxy ?
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
: null; : null;
@@ -232,7 +236,7 @@ export function loadConfig(): Config {
apiUrl: `${scheme}://${host}/api`, apiUrl: `${scheme}://${host}/api`,
authUrl: `${scheme}://${host}/auth`, authUrl: `${scheme}://${host}/auth`,
driveUrl: `${scheme}://${host}/files`, driveUrl: `${scheme}://${host}/files`,
db: config.db, db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass },
dbReplications: config.dbReplications, dbReplications: config.dbReplications,
dbSlaves: config.dbSlaves, dbSlaves: config.dbSlaves,
meilisearch: config.meilisearch, meilisearch: config.meilisearch,
@@ -260,7 +264,7 @@ export function loadConfig(): Config {
deliverJobMaxAttempts: config.deliverJobMaxAttempts, deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts, inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles, proxyRemoteFiles: config.proxyRemoteFiles,
signToActivityPubGet: config.signToActivityPubGet, signToActivityPubGet: config.signToActivityPubGet ?? true,
mediaProxy: externalMediaProxy ?? internalMediaProxy, mediaProxy: externalMediaProxy ?? internalMediaProxy,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
videoThumbnailGenerator: config.videoThumbnailGenerator ? videoThumbnailGenerator: config.videoThumbnailGenerator ?

View File

@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
// dummy
export const MAX_NOTE_TEXT_LENGTH = 3000; export const MAX_NOTE_TEXT_LENGTH = 3000;
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min

View File

@@ -40,6 +40,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
firstRetrievedAt: new Date(parsed.firstRetrievedAt), firstRetrievedAt: new Date(parsed.firstRetrievedAt),
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null, latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null, infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
}; };
}, },
}); });

View File

@@ -47,6 +47,7 @@ export type RolePolicies = {
canHideAds: boolean; canHideAds: boolean;
driveCapacityMb: number; driveCapacityMb: number;
alwaysMarkNsfw: boolean; alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean;
pinLimit: number; pinLimit: number;
antennaLimit: number; antennaLimit: number;
wordMuteLimit: number; wordMuteLimit: number;
@@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canHideAds: false, canHideAds: false,
driveCapacityMb: 100, driveCapacityMb: 100,
alwaysMarkNsfw: false, alwaysMarkNsfw: false,
canUpdateBioMedia: true,
pinLimit: 5, pinLimit: 5,
antennaLimit: 5, antennaLimit: 5,
wordMuteLimit: 200, wordMuteLimit: 200,
@@ -376,6 +378,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)), pinLimit: calc('pinLimit', vs => Math.max(...vs)),
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)), antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)), wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),

View File

@@ -25,7 +25,7 @@ export class ApMfmService {
} }
@bindThis @bindThis
public getNoteHtml(note: MiNote, apAppend?: string) { public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) {
let noMisskeyContent = false; let noMisskeyContent = false;
const srcMfm = (note.text ?? '') + (apAppend ?? ''); const srcMfm = (note.text ?? '') + (apAppend ?? '');

View File

@@ -34,6 +34,7 @@ import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js'; import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js';
@@ -100,6 +101,8 @@ export class ApPersonService implements OnModuleInit {
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
private roleService: RoleService,
) { ) {
} }
@@ -238,6 +241,11 @@ export class ApPersonService implements OnModuleInit {
return this.apImageService.resolveImage(user, img).catch(() => null); return this.apImageService.resolveImage(user, img).catch(() => null);
})); }));
if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null))
&& !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) {
return {};
}
/* /*
we don't want to return nulls on errors! if the database fields we don't want to return nulls on errors! if the database fields
are already null, nothing changes; if the database has old are already null, nothing changes; if the database has old

View File

@@ -501,11 +501,15 @@ export class UserEntityService implements OnModuleInit {
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user), onlineStatus: this.getOnlineStatus(user),
// パフォーマンス上の理由でローカルユーザーのみ // パフォーマンス上の理由でローカルユーザーのみ
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({ badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
.filter((r) => r.isPublic || iAmModerator)
.sort((a, b) => b.displayOrder - a.displayOrder)
.map((r) => ({
name: r.name, name: r.name,
iconUrl: r.iconUrl, iconUrl: r.iconUrl,
displayOrder: r.displayOrder, displayOrder: r.displayOrder,
}))) : undefined, }))
) : undefined,
...(isDetailed ? { ...(isDetailed ? {
url: profile!.url, url: profile!.url,

View File

@@ -4,6 +4,10 @@
*/ */
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean { export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
if (!note) {
return false;
}
if (userIds.has(note.userId) && !ignoreAuthor) { if (userIds.has(note.userId) && !ignoreAuthor) {
return true; return true;
} }

View File

@@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
canUpdateBioMedia: {
type: 'boolean',
optional: false, nullable: false,
},
pinLimit: { pinLimit: {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: false, nullable: false,

View File

@@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import { HashtagService } from '@/core/HashtagService.js'; import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js'; import { RolePolicies, RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
@@ -256,6 +256,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profileUpdates = {} as Partial<MiUserProfile>; const profileUpdates = {} as Partial<MiUserProfile>;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
let policies: RolePolicies | null = null;
if (ps.name !== undefined) { if (ps.name !== undefined) {
if (ps.name === null) { if (ps.name === null) {
@@ -296,14 +297,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.mutedWords !== undefined) { if (ps.mutedWords !== undefined) {
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); policies ??= await this.roleService.getUserPolicies(user.id);
checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit);
validateMuteWordRegex(ps.mutedWords); validateMuteWordRegex(ps.mutedWords);
profileUpdates.mutedWords = ps.mutedWords; profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0; profileUpdates.enableWordMute = ps.mutedWords.length > 0;
} }
if (ps.hardMutedWords !== undefined) { if (ps.hardMutedWords !== undefined) {
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); policies ??= await this.roleService.getUserPolicies(user.id);
checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit);
validateMuteWordRegex(ps.hardMutedWords); validateMuteWordRegex(ps.hardMutedWords);
profileUpdates.hardMutedWords = ps.hardMutedWords; profileUpdates.hardMutedWords = ps.hardMutedWords;
} }
@@ -322,13 +325,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === 'boolean') { if (typeof ps.alwaysMarkNsfw === 'boolean') {
if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); policies ??= await this.roleService.getUserPolicies(user.id);
if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
} }
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
if (ps.avatarId) { if (ps.avatarId) {
policies ??= await this.roleService.getUserPolicies(user.id);
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId }); const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
@@ -344,6 +351,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.bannerId) { if (ps.bannerId) {
policies ??= await this.roleService.getUserPolicies(user.id);
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId }); const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
@@ -359,14 +369,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.avatarDecorations) { if (ps.avatarDecorations) {
policies ??= await this.roleService.getUserPolicies(user.id);
const decorations = await this.avatarDecorationService.getAll(true); const decorations = await this.avatarDecorationService.getAll(true);
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const myRoles = await this.roleService.getUserRoles(user.id);
const allRoles = await this.roleService.getRoles(); const allRoles = await this.roleService.getRoles();
const decorationIds = decorations const decorationIds = decorations
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
.map(d => d.id); .map(d => d.id);
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
id: d.id, id: d.id,

View File

@@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>();
const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
if (!iAmModerator) { if (!iAmModerator) {
const user = await this.cacheService.findUserById(ps.userId); const user = await this.cacheService.findUserById(ps.userId);
@@ -85,7 +87,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) { if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
throw new ApiError(meta.errors.reactionsNotPublic); throw new ApiError(meta.errors.reactionsNotPublic);
} }
// early return if me is blocked by requesting user
if (userIdsWhoBlockingMe.has(ps.userId)) {
return [];
} }
}
const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>();
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
@@ -94,9 +103,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me); this.queryService.generateVisibilityQuery(query, me);
const reactions = await query const reactions = (await query
.limit(ps.limit) .limit(ps.limit)
.getMany(); .getMany()).filter(reaction => {
if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user
if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false;
if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false;
return true;
});
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true }); return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
}); });

View File

@@ -206,7 +206,7 @@ describe('2要素認証', () => {
username, username,
}, alice); }, alice);
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
const signinResponse = await api('signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
@@ -248,7 +248,7 @@ describe('2要素認証', () => {
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}) as any, alice); } as any) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName); assert.strictEqual(keyDoneResponse.body.name, keyName);
@@ -257,22 +257,22 @@ describe('2要素認証', () => {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, true); assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
const signinResponse = await api('signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined); assert.strictEqual(signinResponse.body.i, undefined);
assert.notEqual(signinResponse.body.challenge, undefined); assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
assert.notEqual(signinResponse.body.allowCredentials, undefined); assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url')); assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
const signinResponse2 = await api('signin', signinWithSecurityKeyParam({ const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
keyName, keyName,
credentialId, credentialId,
requestOptions: signinResponse.body, requestOptions: signinResponse.body,
})); } as any));
assert.strictEqual(signinResponse2.status, 200); assert.strictEqual(signinResponse2.status, 200);
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
@@ -307,7 +307,7 @@ describe('2要素認証', () => {
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}) as any, alice); } as any) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
const passwordLessResponse = await api('i/2fa/password-less', { const passwordLessResponse = await api('i/2fa/password-less', {
@@ -319,7 +319,7 @@ describe('2要素認証', () => {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true); assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
const signinResponse = await api('signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
@@ -333,7 +333,7 @@ describe('2要素認証', () => {
keyName, keyName,
credentialId, credentialId,
requestOptions: signinResponse.body, requestOptions: signinResponse.body,
}), } as any),
password: '', password: '',
}); });
assert.strictEqual(signinResponse2.status, 200); assert.strictEqual(signinResponse2.status, 200);
@@ -370,7 +370,7 @@ describe('2要素認証', () => {
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}) as any, alice); } as any) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
const renamedKey = 'other-key'; const renamedKey = 'other-key';
@@ -383,6 +383,7 @@ describe('2要素認証', () => {
const iResponse = await api('i', { const iResponse = await api('i', {
}, alice); }, alice);
assert.strictEqual(iResponse.status, 200); assert.strictEqual(iResponse.status, 200);
assert.ok(iResponse.body.securityKeysList);
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url')); const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
assert.strictEqual(securityKeys.length, 1); assert.strictEqual(securityKeys.length, 1);
assert.strictEqual(securityKeys[0].name, renamedKey); assert.strictEqual(securityKeys[0].name, renamedKey);
@@ -419,13 +420,14 @@ describe('2要素認証', () => {
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}) as any, alice); } as any) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す // テストの実行順によっては複数残ってるので全部消す
const iResponse = await api('i', { const iResponse = await api('i', {
}, alice); }, alice);
assert.strictEqual(iResponse.status, 200); assert.strictEqual(iResponse.status, 200);
assert.ok(iResponse.body.securityKeysList);
for (const key of iResponse.body.securityKeysList) { for (const key of iResponse.body.securityKeysList) {
const removeKeyResponse = await api('i/2fa/remove-key', { const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
@@ -439,7 +441,7 @@ describe('2要素認証', () => {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, false); assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
const signinResponse = await api('signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
@@ -470,7 +472,7 @@ describe('2要素認証', () => {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
const unregisterResponse = await api('i/2fa/unregister', { const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),

View File

@@ -410,21 +410,21 @@ describe('API visibility', () => {
test('[HTL] public-post が 自分が見れる', async () => { test('[HTL] public-post が 自分が見れる', async () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter(n => n.id === pub.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[HTL] public-post が 非フォロワーから見れない', async () => { test('[HTL] public-post が 非フォロワーから見れない', async () => {
const res = await api('notes/timeline', { limit: 100 }, other); const res = await api('notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter(n => n.id === pub.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
test('[HTL] followers-post が フォロワーから見れる', async () => { test('[HTL] followers-post が フォロワーから見れる', async () => {
const res = await api('notes/timeline', { limit: 100 }, follower); const res = await api('notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === fol.id); const notes = res.body.filter(n => n.id === fol.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
//#endregion //#endregion
@@ -433,21 +433,21 @@ describe('API visibility', () => {
test('[replies] followers-reply が フォロワーから見れる', async () => { test('[replies] followers-reply が フォロワーから見れる', async () => {
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower); const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter(n => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other); const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter(n => n.id === folR.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target); const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter(n => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
//#endregion //#endregion
@@ -456,14 +456,14 @@ describe('API visibility', () => {
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await api('notes/mentions', { limit: 100 }, target); const res = await api('notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter(n => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
const res = await api('notes/mentions', { limit: 100 }, target); const res = await api('notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folM.id); const notes = res.body.filter(n => n.id === folM.id);
assert.strictEqual(notes[0].text, '@target x'); assert.strictEqual(notes[0].text, '@target x');
}); });
//#endregion //#endregion

View File

@@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { api, post, signup } from '../utils.js'; import { api, castAsError, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Block', () => { describe('Block', () => {
@@ -33,7 +33,7 @@ describe('Block', () => {
const res = await api('following/create', { userId: alice.id }, bob); const res = await api('following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); assert.strictEqual(castAsError(res.body).error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
}); });
test('ブロックされているユーザーにリアクションできない', async () => { test('ブロックされているユーザーにリアクションできない', async () => {
@@ -42,7 +42,8 @@ describe('Block', () => {
const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); assert.ok(res.body);
assert.strictEqual(castAsError(res.body).error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
}); });
test('ブロックされているユーザーに返信できない', async () => { test('ブロックされているユーザーに返信できない', async () => {
@@ -51,7 +52,8 @@ describe('Block', () => {
const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob); const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); assert.ok(res.body);
assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
}); });
test('ブロックされているユーザーのートをRenoteできない', async () => { test('ブロックされているユーザーのートをRenoteできない', async () => {
@@ -60,7 +62,7 @@ describe('Block', () => {
const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob); const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
}); });
// TODO: ユーザーリストに入れられないテスト // TODO: ユーザーリストに入れられないテスト

View File

@@ -79,14 +79,14 @@ describe('クリップ', () => {
}; };
const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => { const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
return await successfulApiCall({ await successfulApiCall({
endpoint: 'clips/delete', endpoint: 'clips/delete',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}) as any as void; });
}; };
const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => { const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
@@ -454,25 +454,25 @@ describe('クリップ', () => {
let aliceClip: Misskey.entities.Clip; let aliceClip: Misskey.entities.Clip;
const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => { const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
return successfulApiCall({ await successfulApiCall({
endpoint: 'clips/favorite', endpoint: 'clips/favorite',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}) as any as void; });
}; };
const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => { const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
return successfulApiCall({ await successfulApiCall({
endpoint: 'clips/unfavorite', endpoint: 'clips/unfavorite',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}) as any as void; });
}; };
const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => { const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {

View File

@@ -10,7 +10,7 @@ import * as assert from 'assert';
// https://github.com/node-fetch/node-fetch/pull/1664 // https://github.com/node-fetch/node-fetch/pull/1664
import { Blob } from 'node-fetch'; import { Blob } from 'node-fetch';
import { MiUser } from '@/models/_.js'; import { MiUser } from '@/models/_.js';
import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js'; import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Endpoints', () => { describe('Endpoints', () => {
@@ -164,7 +164,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.id, alice.id); assert.strictEqual((res.body as unknown as { id: string }).id, alice.id);
}); });
test('ユーザーが存在しなかったら怒る', async () => { test('ユーザーが存在しなかったら怒る', async () => {
@@ -285,7 +285,8 @@ describe('Endpoints', () => {
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'CANNOT_REACT_TO_RENOTE'); assert.ok(res.body);
assert.strictEqual(castAsError(res.body).error.code, 'CANNOT_REACT_TO_RENOTE');
}); });
test('引用にリアクションできる', async () => { test('引用にリアクションできる', async () => {
@@ -1063,7 +1064,7 @@ describe('Endpoints', () => {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
assert.strictEqual(res1.status, 204); assert.strictEqual(res1.status, 204);
assert.strictEqual(res2.body?.memo, memo); assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
}); });
test('自分に関するメモを更新できる', async () => { test('自分に関するメモを更新できる', async () => {
@@ -1078,7 +1079,7 @@ describe('Endpoints', () => {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(res1.status, 204); assert.strictEqual(res1.status, 204);
assert.strictEqual(res2.body?.memo, memo); assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo);
}); });
test('メモを削除できる', async () => { test('メモを削除できる', async () => {
@@ -1099,7 +1100,7 @@ describe('Endpoints', () => {
}, alice); }, alice);
// memoには常に文字列かnullが入っている(5cac151) // memoには常に文字列かnullが入っている(5cac151)
assert.strictEqual(res.body.memo, null); assert.strictEqual((res.body as unknown as { memo: string | null }).memo, null);
}); });
test('メモは個人ごとに独立して保存される', async () => { test('メモは個人ごとに独立して保存される', async () => {
@@ -1126,8 +1127,8 @@ describe('Endpoints', () => {
}, carol), }, carol),
]); ]);
assert.strictEqual(resAlice.body.memo, memoAliceToBob); assert.strictEqual((resAlice.body as unknown as { memo: string }).memo, memoAliceToBob);
assert.strictEqual(resCarol.body.memo, memoCarolToBob); assert.strictEqual((resCarol.body as unknown as { memo: string }).memo, memoCarolToBob);
}); });
}); });
}); });

View File

@@ -61,14 +61,14 @@ describe('export-clips', () => {
}); });
test('basic export', async () => { test('basic export', async () => {
let res = await api('clips/create', { const res1 = await api('clips/create', {
name: 'foo', name: 'foo',
description: 'bar', description: 'bar',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res1.status, 200);
res = await api('i/export-clips', {}, alice); const res2 = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res2.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
assert.strictEqual(exported[0].name, 'foo'); assert.strictEqual(exported[0].name, 'foo');
@@ -77,7 +77,7 @@ describe('export-clips', () => {
}); });
test('export with notes', async () => { test('export with notes', async () => {
let res = await api('clips/create', { const res = await api('clips/create', {
name: 'foo', name: 'foo',
description: 'bar', description: 'bar',
}, alice); }, alice);
@@ -96,15 +96,15 @@ describe('export-clips', () => {
}); });
for (const note of [note1, note2]) { for (const note of [note1, note2]) {
res = await api('clips/add-note', { const res2 = await api('clips/add-note', {
clipId: clip.id, clipId: clip.id,
noteId: note.id, noteId: note.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res2.status, 204);
} }
res = await api('i/export-clips', {}, alice); const res3 = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res3.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
assert.strictEqual(exported[0].name, 'foo'); assert.strictEqual(exported[0].name, 'foo');
@@ -116,19 +116,19 @@ describe('export-clips', () => {
}); });
test('multiple clips', async () => { test('multiple clips', async () => {
let res = await api('clips/create', { const res1 = await api('clips/create', {
name: 'kawaii', name: 'kawaii',
description: 'kawaii', description: 'kawaii',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res1.status, 200);
const clip1 = res.body; const clip1 = res1.body;
res = await api('clips/create', { const res2 = await api('clips/create', {
name: 'yuri', name: 'yuri',
description: 'yuri', description: 'yuri',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res2.status, 200);
const clip2 = res.body; const clip2 = res2.body;
const note1 = await post(alice, { const note1 = await post(alice, {
text: 'baz1', text: 'baz1',
@@ -138,20 +138,26 @@ describe('export-clips', () => {
text: 'baz2', text: 'baz2',
}); });
res = await api('clips/add-note', { {
const res = await api('clips/add-note', {
clipId: clip1.id, clipId: clip1.id,
noteId: note1.id, noteId: note1.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
}
res = await api('clips/add-note', { {
const res = await api('clips/add-note', {
clipId: clip2.id, clipId: clip2.id,
noteId: note2.id, noteId: note2.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
}
res = await api('i/export-clips', {}, alice); {
const res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
}
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
assert.strictEqual(exported[0].name, 'kawaii'); assert.strictEqual(exported[0].name, 'kawaii');
@@ -163,7 +169,7 @@ describe('export-clips', () => {
}); });
test('Clipping other user\'s note', async () => { test('Clipping other user\'s note', async () => {
let res = await api('clips/create', { const res = await api('clips/create', {
name: 'kawaii', name: 'kawaii',
description: 'kawaii', description: 'kawaii',
}, alice); }, alice);
@@ -175,14 +181,14 @@ describe('export-clips', () => {
visibility: 'followers', visibility: 'followers',
}); });
res = await api('clips/add-note', { const res2 = await api('clips/add-note', {
clipId: clip.id, clipId: clip.id,
noteId: note.id, noteId: note.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res2.status, 204);
res = await api('i/export-clips', {}, alice); const res3 = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res3.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
assert.strictEqual(exported[0].name, 'kawaii'); assert.strictEqual(exported[0].name, 'kawaii');

View File

@@ -13,14 +13,14 @@ import { loadConfig } from '@/config.js';
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js'; import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { jobQueue } from '@/boot/common.js'; import { jobQueue } from '@/boot/common.js';
import { api, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js'; import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Account Move', () => { describe('Account Move', () => {
let jq: INestApplicationContext; let jq: INestApplicationContext;
let url: URL; let url: URL;
let root: any; let root: misskey.entities.SignupResponse;
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse; let carol: misskey.entities.SignupResponse;
@@ -93,8 +93,8 @@ describe('Account Move', () => {
}, bob); }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
}); });
test('Unable to add duplicated aliases to alsoKnownAs', async () => { test('Unable to add duplicated aliases to alsoKnownAs', async () => {
@@ -103,8 +103,8 @@ describe('Account Move', () => {
}, bob); }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'INVALID_PARAM'); assert.strictEqual(castAsError(res.body).error.code, 'INVALID_PARAM');
assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532'); assert.strictEqual(castAsError(res.body).error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
}); });
test('Unable to add itself', async () => { test('Unable to add itself', async () => {
@@ -113,8 +113,8 @@ describe('Account Move', () => {
}, bob); }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF'); assert.strictEqual(castAsError(res.body).error.code, 'FORBIDDEN_TO_SET_YOURSELF');
assert.strictEqual(res.body.error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4'); assert.strictEqual(castAsError(res.body).error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4');
}); });
test('Unable to add a nonexisting local account to alsoKnownAs', async () => { test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
@@ -123,16 +123,16 @@ describe('Account Move', () => {
}, bob); }, bob);
assert.strictEqual(res1.status, 400); assert.strictEqual(res1.status, 400);
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER'); assert.strictEqual(castAsError(res1.body).error.code, 'NO_SUCH_USER');
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); assert.strictEqual(castAsError(res1.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
const res2 = await api('i/update', { const res2 = await api('i/update', {
alsoKnownAs: ['@alice', 'nonexist'], alsoKnownAs: ['@alice', 'nonexist'],
}, bob); }, bob);
assert.strictEqual(res2.status, 400); assert.strictEqual(res2.status, 400);
assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER'); assert.strictEqual(castAsError(res2.body).error.code, 'NO_SUCH_USER');
assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); assert.strictEqual(castAsError(res2.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
}); });
test('Able to add two existing local account to alsoKnownAs', async () => { test('Able to add two existing local account to alsoKnownAs', async () => {
@@ -241,8 +241,8 @@ describe('Account Move', () => {
}, root); }, root);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN'); assert.strictEqual(castAsError(res.body).error.code, 'NOT_ROOT_FORBIDDEN');
assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24'); assert.strictEqual(castAsError(res.body).error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
}); });
test('Unable to move to a nonexisting local account', async () => { test('Unable to move to a nonexisting local account', async () => {
@@ -251,8 +251,8 @@ describe('Account Move', () => {
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER');
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
}); });
test('Unable to move if alsoKnownAs is invalid', async () => { test('Unable to move if alsoKnownAs is invalid', async () => {
@@ -261,8 +261,8 @@ describe('Account Move', () => {
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS'); assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
}); });
test('Relationships have been properly migrated', async () => { test('Relationships have been properly migrated', async () => {
@@ -279,36 +279,44 @@ describe('Account Move', () => {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(aliceFollowings.status, 200); assert.strictEqual(aliceFollowings.status, 200);
assert.ok(aliceFollowings);
assert.strictEqual(aliceFollowings.body.length, 3); assert.strictEqual(aliceFollowings.body.length, 3);
const carolFollowings = await api('users/following', { const carolFollowings = await api('users/following', {
userId: carol.id, userId: carol.id,
}, carol); }, carol);
assert.strictEqual(carolFollowings.status, 200); assert.strictEqual(carolFollowings.status, 200);
assert.ok(carolFollowings);
assert.strictEqual(carolFollowings.body.length, 2); assert.strictEqual(carolFollowings.body.length, 2);
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id); assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id); assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
const blockings = await api('blocking/list', {}, dave); const blockings = await api('blocking/list', {}, dave);
assert.strictEqual(blockings.status, 200); assert.strictEqual(blockings.status, 200);
assert.ok(blockings);
assert.strictEqual(blockings.body.length, 2); assert.strictEqual(blockings.body.length, 2);
assert.strictEqual(blockings.body[0].blockeeId, bob.id); assert.strictEqual(blockings.body[0].blockeeId, bob.id);
assert.strictEqual(blockings.body[1].blockeeId, alice.id); assert.strictEqual(blockings.body[1].blockeeId, alice.id);
const mutings = await api('mute/list', {}, dave); const mutings = await api('mute/list', {}, dave);
assert.strictEqual(mutings.status, 200); assert.strictEqual(mutings.status, 200);
assert.ok(mutings);
assert.strictEqual(mutings.body.length, 2); assert.strictEqual(mutings.body.length, 2);
assert.strictEqual(mutings.body[0].muteeId, bob.id); assert.strictEqual(mutings.body[0].muteeId, bob.id);
assert.strictEqual(mutings.body[1].muteeId, alice.id); assert.strictEqual(mutings.body[1].muteeId, alice.id);
const rootLists = await api('users/lists/list', {}, root); const rootLists = await api('users/lists/list', {}, root);
assert.strictEqual(rootLists.status, 200); assert.strictEqual(rootLists.status, 200);
assert.ok(rootLists);
assert.ok(rootLists.body[0].userIds);
assert.strictEqual(rootLists.body[0].userIds.length, 2); assert.strictEqual(rootLists.body[0].userIds.length, 2);
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id)); assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id)); assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
const eveLists = await api('users/lists/list', {}, eve); const eveLists = await api('users/lists/list', {}, eve);
assert.strictEqual(eveLists.status, 200); assert.strictEqual(eveLists.status, 200);
assert.ok(eveLists);
assert.ok(eveLists.body[0].userIds);
assert.strictEqual(eveLists.body[0].userIds.length, 1); assert.strictEqual(eveLists.body[0].userIds.length, 1);
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id)); assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
}); });
@@ -347,8 +355,8 @@ describe('Account Move', () => {
}, bob); }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS'); assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS');
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
}); });
test('Follow and follower counts are properly adjusted', async () => { test('Follow and follower counts are properly adjusted', async () => {
@@ -419,8 +427,9 @@ describe('Account Move', () => {
] as const)('Prohibit access after moving: %s', async (endpoint) => { ] as const)('Prohibit access after moving: %s', async (endpoint) => {
const res = await api(endpoint, {}, alice); const res = await api(endpoint, {}, alice);
assert.strictEqual(res.status, 403); assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); assert.ok(res.body);
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
}); });
test('Prohibit access after moving: /antennas/update', async () => { test('Prohibit access after moving: /antennas/update', async () => {
@@ -438,16 +447,19 @@ describe('Account Move', () => {
}, alice); }, alice);
assert.strictEqual(res.status, 403); assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); assert.ok(res.body);
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
}); });
test('Prohibit access after moving: /drive/files/create', async () => { test('Prohibit access after moving: /drive/files/create', async () => {
// FIXME: 一旦逃げておく
const res = await uploadFile(alice); const res = await uploadFile(alice);
assert.strictEqual(res.status, 403); assert.strictEqual(res.status, 403);
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED'); assert.ok(res.body);
assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
}); });
test('Prohibit updating alsoKnownAs after moving', async () => { test('Prohibit updating alsoKnownAs after moving', async () => {
@@ -456,8 +468,8 @@ describe('Account Move', () => {
}, alice); }, alice);
assert.strictEqual(res.status, 403); assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
}); });
}); });
}); });

View File

@@ -47,8 +47,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
@@ -92,9 +92,9 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => { test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
@@ -108,9 +108,9 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
}); });
@@ -124,8 +124,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからのリプライが含まれない', async () => { test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
@@ -138,8 +138,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからのリプライが含まれない', async () => { test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
@@ -152,8 +152,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => { test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
@@ -166,8 +166,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからのリノートが含まれない', async () => { test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
@@ -180,8 +180,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
@@ -193,8 +193,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob); await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol); await api('following/delete', { userId: alice.id }, carol);
@@ -210,8 +210,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob); await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol); await api('following/delete', { userId: alice.id }, carol);
@@ -228,8 +228,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからのリプライが含まれない', async () => { test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
const aliceNote = await post(alice, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi' });
@@ -241,8 +241,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからのリプライが含まれない', async () => { test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
@@ -255,8 +255,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => { test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
@@ -269,8 +269,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからのリノートが含まれない', async () => { test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
@@ -283,8 +283,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
@@ -296,8 +296,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob); await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol); await api('following/delete', { userId: alice.id }, carol);
@@ -313,8 +313,8 @@ describe('Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false);
}); });
}); });
}); });

View File

@@ -3,16 +3,18 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { Repository } from "typeorm";
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { MiNote } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { api, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Note', () => { describe('Note', () => {
let Notes: any; let Notes: Repository<MiNote>;
let root: misskey.entities.SignupResponse; let root: misskey.entities.SignupResponse;
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
@@ -61,8 +63,8 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
}, 1000 * 10); }, 1000 * 10);
test('存在しないファイルで怒られる', async () => { test('存在しないファイルで怒られる', async () => {
@@ -72,8 +74,8 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
}); });
test('不正なファイルIDで怒られる', async () => { test('不正なファイルIDで怒られる', async () => {
@@ -81,8 +83,8 @@ describe('Note', () => {
fileIds: ['kyoppie'], fileIds: ['kyoppie'],
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE');
assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
}); });
test('返信できる', async () => { test('返信できる', async () => {
@@ -101,6 +103,7 @@ describe('Note', () => {
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, alicePost.text); assert.strictEqual(res.body.createdNote.text, alicePost.text);
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
assert.ok(res.body.createdNote.reply);
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
}); });
@@ -118,6 +121,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
assert.ok(res.body.createdNote.renote);
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}); });
@@ -137,6 +141,7 @@ describe('Note', () => {
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, alicePost.text); assert.strictEqual(res.body.createdNote.text, alicePost.text);
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
assert.ok(res.body.createdNote.renote);
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}); });
@@ -218,7 +223,7 @@ describe('Note', () => {
}, bob); }, bob);
assert.strictEqual(bobReply.status, 400); assert.strictEqual(bobReply.status, 400);
assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE'); assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE');
}); });
test('visibility: specifiedなートに対してvisibility: specifiedで返信できる', async () => { test('visibility: specifiedなートに対してvisibility: specifiedで返信できる', async () => {
@@ -256,7 +261,7 @@ describe('Note', () => {
}, bob); }, bob);
assert.strictEqual(bobReply.status, 400); assert.strictEqual(bobReply.status, 400);
assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY'); assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY');
}); });
test('文字数ぎりぎりで怒られない', async () => { test('文字数ぎりぎりで怒られない', async () => {
@@ -333,6 +338,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.text, post.text); assert.strictEqual(res.body.createdNote.text, post.text);
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id }); const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
assert.ok(noteDoc);
assert.deepStrictEqual(noteDoc.mentions, [bob.id]); assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
}); });
@@ -345,6 +351,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.ok(res.body.createdNote.files);
assert.strictEqual(res.body.createdNote.files.length, 1); assert.strictEqual(res.body.createdNote.files.length, 1);
assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id); assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
}); });
@@ -363,8 +370,9 @@ describe('Note', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id); const myNote = res.body.find(note => note.id === createdNote.body.createdNote.id);
assert.notEqual(myNote, null); assert.ok(myNote);
assert.ok(myNote.files);
assert.strictEqual(myNote.files.length, 1); assert.strictEqual(myNote.files.length, 1);
assert.strictEqual(myNote.files[0].id, file.body!.id); assert.strictEqual(myNote.files[0].id, file.body!.id);
}); });
@@ -389,7 +397,9 @@ describe('Note', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null); assert.ok(myNote);
assert.ok(myNote.renote);
assert.ok(myNote.renote.files);
assert.strictEqual(myNote.renote.files.length, 1); assert.strictEqual(myNote.renote.files.length, 1);
assert.strictEqual(myNote.renote.files[0].id, file.body!.id); assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
}); });
@@ -415,7 +425,9 @@ describe('Note', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id); const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
assert.notEqual(myNote, null); assert.ok(myNote);
assert.ok(myNote.reply);
assert.ok(myNote.reply.files);
assert.strictEqual(myNote.reply.files.length, 1); assert.strictEqual(myNote.reply.files.length, 1);
assert.strictEqual(myNote.reply.files[0].id, file.body!.id); assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
}); });
@@ -446,7 +458,10 @@ describe('Note', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null); assert.ok(myNote);
assert.ok(myNote.renote);
assert.ok(myNote.renote.reply);
assert.ok(myNote.renote.reply.files);
assert.strictEqual(myNote.renote.reply.files.length, 1); assert.strictEqual(myNote.renote.reply.files.length, 1);
assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id); assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
}); });
@@ -474,7 +489,7 @@ describe('Note', () => {
priority: 0, priority: 0,
value: true, value: true,
}, },
} as any, },
}, root); }, root);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@@ -498,7 +513,7 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(liftnsfw.status, 400); assert.strictEqual(liftnsfw.status, 400);
assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE'); assert.strictEqual(castAsError(liftnsfw.body).error.code, 'RESTRICTED_BY_ROLE');
const oldaddnsfw = await api('drive/files/update', { const oldaddnsfw = await api('drive/files/update', {
fileId: file.body!.id, fileId: file.body!.id,
@@ -710,7 +725,7 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(note1.status, 400); assert.strictEqual(note1.status, 400);
assert.strictEqual(note1.body.error.code, 'CONTAINS_PROHIBITED_WORDS'); assert.strictEqual(castAsError(note1.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
}); });
test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => { test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => {
@@ -727,7 +742,7 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(note2.status, 400); assert.strictEqual(note2.status, 400);
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS'); assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
}); });
test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => { test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => {
@@ -744,7 +759,7 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(note2.status, 400); assert.strictEqual(note2.status, 400);
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS'); assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS');
}); });
test('禁止ワードを含んでるリモートノートもエラーになる', async () => { test('禁止ワードを含んでるリモートノートもエラーになる', async () => {
@@ -786,7 +801,7 @@ describe('Note', () => {
priority: 1, priority: 1,
value: 0, value: 0,
}, },
} as any, },
}, root); }, root);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@@ -807,7 +822,7 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(note.status, 400); assert.strictEqual(note.status, 400);
assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS'); assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS');
await api('admin/roles/unassign', { await api('admin/roles/unassign', {
userId: alice.id, userId: alice.id,
@@ -840,7 +855,7 @@ describe('Note', () => {
priority: 1, priority: 1,
value: 0, value: 0,
}, },
} as any, },
}, root); }, root);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@@ -863,7 +878,7 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(note.status, 400); assert.strictEqual(note.status, 400);
assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS'); assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS');
await api('admin/roles/unassign', { await api('admin/roles/unassign', {
userId: alice.id, userId: alice.id,
@@ -896,7 +911,7 @@ describe('Note', () => {
priority: 1, priority: 1,
value: 1, value: 1,
}, },
} as any, },
}, root); }, root);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@@ -951,6 +966,7 @@ describe('Note', () => {
assert.strictEqual(deleteOneRes.status, 204); assert.strictEqual(deleteOneRes.status, 204);
let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
assert.ok(mainNote);
assert.strictEqual(mainNote.repliesCount, 1); assert.strictEqual(mainNote.repliesCount, 1);
const deleteTwoRes = await api('notes/delete', { const deleteTwoRes = await api('notes/delete', {
@@ -959,6 +975,7 @@ describe('Note', () => {
assert.strictEqual(deleteTwoRes.status, 204); assert.strictEqual(deleteTwoRes.status, 204);
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
assert.ok(mainNote);
assert.strictEqual(mainNote.repliesCount, 0); assert.strictEqual(mainNote.repliesCount, 0);
}); });
}); });
@@ -980,7 +997,7 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'UNAVAILABLE'); assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
}); });
afterAll(async () => { afterAll(async () => {
@@ -992,7 +1009,7 @@ describe('Note', () => {
const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice); const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'NO_SUCH_NOTE'); assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_NOTE');
}); });
test('不可視なノートは翻訳できない', async () => { test('不可視なノートは翻訳できない', async () => {
@@ -1000,7 +1017,7 @@ describe('Note', () => {
const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob); const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob);
assert.strictEqual(bobTranslateAttempt.status, 400); assert.strictEqual(bobTranslateAttempt.status, 400);
assert.strictEqual(bobTranslateAttempt.body.error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE'); assert.strictEqual(castAsError(bobTranslateAttempt.body).error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE');
}); });
test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => { test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => {
@@ -1016,7 +1033,7 @@ describe('Note', () => {
// NOTE: デフォルトでは登録されていないので落ちる // NOTE: デフォルトでは登録されていないので落ちる
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'UNAVAILABLE'); assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE');
}); });
}); });
}); });

View File

@@ -42,9 +42,9 @@ describe('Renote Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false); assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
}); });
test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => { test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
@@ -59,9 +59,9 @@ describe('Renote Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true); assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
}); });
// #12956 // #12956
@@ -76,8 +76,8 @@ describe('Renote Mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true);
}); });
test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => { test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {

View File

@@ -33,9 +33,9 @@ describe('Note thread mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); assert.strictEqual(res.body.some(note => note.id === carolReply.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false);
}); });
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
@@ -93,8 +93,8 @@ describe('Note thread mute', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReply.id), false);
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReplyWithoutMention.id), false);
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
}); });

View File

@@ -37,8 +37,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
}); });
test.concurrent('フォローしているユーザーのノートが含まれる', async () => { test.concurrent('フォローしているユーザーのノートが含まれる', async () => {
@@ -53,8 +53,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
@@ -69,9 +69,9 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => { test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => {
@@ -86,8 +86,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => { test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => {
@@ -103,8 +103,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => { test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => {
@@ -120,8 +120,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
@@ -137,8 +137,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
@@ -156,9 +156,9 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi');
}); });
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => { test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
@@ -175,8 +175,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
}); });
test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => { test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => {
@@ -191,8 +191,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
}); });
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
@@ -207,8 +207,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('自分の他人への返信が含まれる', async () => { test.concurrent('自分の他人への返信が含まれる', async () => {
@@ -221,8 +221,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
}); });
test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => { test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => {
@@ -237,8 +237,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => { test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => {
@@ -255,8 +255,8 @@ describe('Timelines', () => {
withRenotes: false, withRenotes: false,
}, alice); }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => { test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => {
@@ -273,8 +273,8 @@ describe('Timelines', () => {
withRenotes: false, withRenotes: false,
}, alice); }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => { test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => {
@@ -288,7 +288,7 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
@@ -304,8 +304,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
@@ -322,8 +322,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
@@ -338,7 +338,7 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
@@ -353,7 +353,7 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => { test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => {
@@ -374,10 +374,10 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice); const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
}, 1000 * 10); }, 1000 * 10);
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => { test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
@@ -392,7 +392,7 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('自分の visibility: specified なノートが含まれる', async () => { test.concurrent('自分の visibility: specified なノートが含まれる', async () => {
@@ -404,8 +404,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
}); });
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => { test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => {
@@ -419,8 +419,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
}); });
test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => { test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => {
@@ -432,7 +432,7 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => { test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => {
@@ -446,7 +446,7 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => { test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => {
@@ -459,8 +459,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'ok'); assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok');
}); });
/* TODO /* TODO
@@ -474,8 +474,8 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'ok'); assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok');
}); });
*/ */
@@ -490,7 +490,7 @@ describe('Timelines', () => {
const res = await api('notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
}); });
@@ -505,8 +505,8 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('他人の他人への返信が含まれない', async () => { test.concurrent('他人の他人への返信が含まれない', async () => {
@@ -519,8 +519,8 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
}); });
test.concurrent('他人のその人自身への返信が含まれる', async () => { test.concurrent('他人のその人自身への返信が含まれる', async () => {
@@ -533,8 +533,8 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
}); });
test.concurrent('チャンネル投稿が含まれない', async () => { test.concurrent('チャンネル投稿が含まれない', async () => {
@@ -547,7 +547,7 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('リモートユーザーのノートが含まれない', async () => { test.concurrent('リモートユーザーのノートが含まれない', async () => {
@@ -559,7 +559,7 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
// 含まれても良いと思うけど実装が面倒なので含まれない // 含まれても良いと思うけど実装が面倒なので含まれない
@@ -575,8 +575,8 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('ミュートしているユーザーのノートが含まれない', async () => { test.concurrent('ミュートしているユーザーのノートが含まれない', async () => {
@@ -591,8 +591,8 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
@@ -608,8 +608,8 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
@@ -626,8 +626,8 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
}); });
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
@@ -642,8 +642,8 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
@@ -656,7 +656,7 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice); const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
@@ -670,8 +670,8 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice); const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
}, 1000 * 10); }, 1000 * 10);
}); });
@@ -685,7 +685,7 @@ describe('Timelines', () => {
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => { test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => {
@@ -697,7 +697,7 @@ describe('Timelines', () => {
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => { test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => {
@@ -711,7 +711,7 @@ describe('Timelines', () => {
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
@@ -726,8 +726,8 @@ describe('Timelines', () => {
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('他人の他人への返信が含まれない', async () => { test.concurrent('他人の他人への返信が含まれない', async () => {
@@ -740,8 +740,8 @@ describe('Timelines', () => {
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
}); });
test.concurrent('リモートユーザーのノートが含まれない', async () => { test.concurrent('リモートユーザーのノートが含まれない', async () => {
@@ -753,7 +753,7 @@ describe('Timelines', () => {
const res = await api('notes/local-timeline', { limit: 100 }, alice); const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
@@ -768,7 +768,7 @@ describe('Timelines', () => {
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
@@ -783,7 +783,7 @@ describe('Timelines', () => {
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
@@ -796,7 +796,7 @@ describe('Timelines', () => {
const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice); const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
@@ -810,8 +810,8 @@ describe('Timelines', () => {
const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice); const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
}, 1000 * 10); }, 1000 * 10);
}); });
@@ -828,7 +828,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => { test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => {
@@ -843,7 +843,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => { test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
@@ -858,7 +858,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
@@ -874,7 +874,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => { test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
@@ -890,8 +890,8 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
}); });
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => { test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => {
@@ -908,7 +908,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
@@ -925,7 +925,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => { test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
@@ -942,7 +942,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => { test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
@@ -958,7 +958,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => { test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => {
@@ -974,8 +974,8 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
}); });
test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => { test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => {
@@ -990,8 +990,8 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
}); });
test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => { test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => {
@@ -1007,7 +1007,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => { test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
@@ -1023,8 +1023,8 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
}, 1000 * 10); }, 1000 * 10);
test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => { test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
@@ -1039,8 +1039,8 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
}); });
test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => { test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => {
@@ -1056,7 +1056,7 @@ describe('Timelines', () => {
const res = await api('notes/user-list-timeline', { listId: list.id }, alice); const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
}); });
@@ -1070,7 +1070,7 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id }, alice); const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => { test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
@@ -1082,7 +1082,7 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id }, alice); const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
@@ -1096,8 +1096,8 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id }, alice); const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
}); });
test.concurrent('自身の visibility: followers なノートが含まれる', async () => { test.concurrent('自身の visibility: followers なノートが含まれる', async () => {
@@ -1109,8 +1109,8 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: alice.id }, alice); const res = await api('users/notes', { userId: alice.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
}); });
test.concurrent('チャンネル投稿が含まれない', async () => { test.concurrent('チャンネル投稿が含まれない', async () => {
@@ -1123,7 +1123,7 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id }, alice); const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => { test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
@@ -1137,8 +1137,8 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id }, alice); const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
}); });
test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => { test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => {
@@ -1152,8 +1152,8 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
}); });
test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => { test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => {
@@ -1167,8 +1167,8 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
}); });
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
@@ -1182,8 +1182,8 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice); const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
}, 1000 * 10); }, 1000 * 10);
test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => { test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
@@ -1196,7 +1196,7 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => { test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => {
@@ -1209,7 +1209,7 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => { test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => {
@@ -1222,7 +1222,7 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob); const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
}); });
test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => { test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => {
@@ -1237,7 +1237,7 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id }, alice); const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => { test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
@@ -1253,9 +1253,9 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id }, alice); const res = await api('users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true); assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true);
}); });
test.concurrent('自身の visibility: specified なノートが含まれる', async () => { test.concurrent('自身の visibility: specified なノートが含まれる', async () => {
@@ -1267,7 +1267,7 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice); const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
}); });
test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => { test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => {
@@ -1279,7 +1279,7 @@ describe('Timelines', () => {
const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
/** @see https://github.com/misskey-dev/misskey/issues/14000 */ /** @see https://github.com/misskey-dev/misskey/issues/14000 */

View File

@@ -231,7 +231,7 @@ describe('ユーザー', () => {
rolePublic = await role(root, { isPublic: true, name: 'Public Role' }); rolePublic = await role(root, { isPublic: true, name: 'Public Role' });
await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root); await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root);
userRoleBadge = await signup({ username: 'userRoleBadge' }); userRoleBadge = await signup({ username: 'userRoleBadge' });
roleBadge = await role(root, { asBadge: true, name: 'Badge Role' }); roleBadge = await role(root, { asBadge: true, name: 'Badge Role', isPublic: true });
await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root); await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
userSilenced = await signup({ username: 'userSilenced' }); userSilenced = await signup({ username: 'userSilenced' });
await post(userSilenced, { text: 'test' }); await post(userSilenced, { text: 'test' });
@@ -655,7 +655,16 @@ describe('ユーザー', () => {
iconUrl: roleBadge.iconUrl, iconUrl: roleBadge.iconUrl,
displayOrder: roleBadge.displayOrder, displayOrder: roleBadge.displayOrder,
}]); }]);
assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない assert.deepStrictEqual(response.roles, [{
id: roleBadge.id,
name: roleBadge.name,
color: roleBadge.color,
iconUrl: roleBadge.iconUrl,
description: roleBadge.description,
isModerator: roleBadge.isModerator,
isAdministrator: roleBadge.isAdministrator,
displayOrder: roleBadge.displayOrder,
}]);
}); });
test('をID指定のリスト形式で取得することができる', async () => { test('をID指定のリスト形式で取得することができる', async () => {
const parameters = { userIds: [] }; const parameters = { userIds: [] };

View File

@@ -23,10 +23,10 @@ describe('ApMfmService', () => {
describe('getNoteHtml', () => { describe('getNoteHtml', () => {
test('Do not provide _misskey_content for simple text', () => { test('Do not provide _misskey_content for simple text', () => {
const note: MiNote = { const note = {
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com', text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
mentionedRemoteUsers: '[]', mentionedRemoteUsers: '[]',
} as any; };
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
@@ -35,10 +35,10 @@ describe('ApMfmService', () => {
}); });
test('Provide _misskey_content for MFM', () => { test('Provide _misskey_content for MFM', () => {
const note: MiNote = { const note = {
text: '$[tada foo]', text: '$[tada foo]',
mentionedRemoteUsers: '[]', mentionedRemoteUsers: '[]',
} as any; };
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);

View File

@@ -12,7 +12,7 @@ import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { afterAll, beforeAll, describe, test } from '@jest/globals'; import { afterAll, beforeAll, describe, test } from '@jest/globals';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { FileInfoService } from '@/core/FileInfoService.js'; import { FileInfo, FileInfoService } from '@/core/FileInfoService.js';
//import { DI } from '@/di-symbols.js'; //import { DI } from '@/di-symbols.js';
import { AiService } from '@/core/AiService.js'; import { AiService } from '@/core/AiService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
@@ -28,6 +28,15 @@ const moduleMocker = new ModuleMocker(global);
describe('FileInfoService', () => { describe('FileInfoService', () => {
let app: TestingModule; let app: TestingModule;
let fileInfoService: FileInfoService; let fileInfoService: FileInfoService;
const strip = (fileInfo: FileInfo): Omit<Partial<FileInfo>, 'warnings' | 'blurhash' | 'sensitive' | 'porn'> => {
const fi: Partial<FileInfo> = fileInfo;
delete fi.warnings;
delete fi.sensitive;
delete fi.blurhash;
delete fi.porn;
return fi;
}
beforeAll(async () => { beforeAll(async () => {
app = await Test.createTestingModule({ app = await Test.createTestingModule({
@@ -63,11 +72,7 @@ describe('FileInfoService', () => {
test('Empty file', async () => { test('Empty file', async () => {
const path = `${resources}/emptyfile`; const path = `${resources}/emptyfile`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 0, size: 0,
md5: 'd41d8cd98f00b204e9800998ecf8427e', md5: 'd41d8cd98f00b204e9800998ecf8427e',
@@ -84,11 +89,7 @@ describe('FileInfoService', () => {
describe('IMAGE', () => { describe('IMAGE', () => {
test('Generic JPEG', async () => { test('Generic JPEG', async () => {
const path = `${resources}/192.jpg`; const path = `${resources}/192.jpg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 5131, size: 5131,
md5: '8c9ed0677dd2b8f9f7472c3af247e5e3', md5: '8c9ed0677dd2b8f9f7472c3af247e5e3',
@@ -104,11 +105,7 @@ describe('FileInfoService', () => {
test('Generic APNG', async () => { test('Generic APNG', async () => {
const path = `${resources}/anime.png`; const path = `${resources}/anime.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 1868, size: 1868,
md5: '08189c607bea3b952704676bb3c979e0', md5: '08189c607bea3b952704676bb3c979e0',
@@ -124,11 +121,7 @@ describe('FileInfoService', () => {
test('Generic AGIF', async () => { test('Generic AGIF', async () => {
const path = `${resources}/anime.gif`; const path = `${resources}/anime.gif`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 2248, size: 2248,
md5: '32c47a11555675d9267aee1a86571e7e', md5: '32c47a11555675d9267aee1a86571e7e',
@@ -144,11 +137,7 @@ describe('FileInfoService', () => {
test('PNG with alpha', async () => { test('PNG with alpha', async () => {
const path = `${resources}/with-alpha.png`; const path = `${resources}/with-alpha.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 3772, size: 3772,
md5: 'f73535c3e1e27508885b69b10cf6e991', md5: 'f73535c3e1e27508885b69b10cf6e991',
@@ -164,11 +153,7 @@ describe('FileInfoService', () => {
test('Generic SVG', async () => { test('Generic SVG', async () => {
const path = `${resources}/image.svg`; const path = `${resources}/image.svg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 505, size: 505,
md5: 'b6f52b4b021e7b92cdd04509c7267965', md5: 'b6f52b4b021e7b92cdd04509c7267965',
@@ -185,11 +170,7 @@ describe('FileInfoService', () => {
test('SVG with XML definition', async () => { test('SVG with XML definition', async () => {
// https://github.com/misskey-dev/misskey/issues/4413 // https://github.com/misskey-dev/misskey/issues/4413
const path = `${resources}/with-xml-def.svg`; const path = `${resources}/with-xml-def.svg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 544, size: 544,
md5: '4b7a346cde9ccbeb267e812567e33397', md5: '4b7a346cde9ccbeb267e812567e33397',
@@ -205,11 +186,7 @@ describe('FileInfoService', () => {
test('Dimension limit', async () => { test('Dimension limit', async () => {
const path = `${resources}/25000x25000.png`; const path = `${resources}/25000x25000.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 75933, size: 75933,
md5: '268c5dde99e17cf8fe09f1ab3f97df56', md5: '268c5dde99e17cf8fe09f1ab3f97df56',
@@ -225,11 +202,7 @@ describe('FileInfoService', () => {
test('Rotate JPEG', async () => { test('Rotate JPEG', async () => {
const path = `${resources}/rotate.jpg`; const path = `${resources}/rotate.jpg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 12624, size: 12624,
md5: '68d5b2d8d1d1acbbce99203e3ec3857e', md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
@@ -247,11 +220,7 @@ describe('FileInfoService', () => {
describe('AUDIO', () => { describe('AUDIO', () => {
test('MP3', async () => { test('MP3', async () => {
const path = `${resources}/kick_gaba7.mp3`; const path = `${resources}/kick_gaba7.mp3`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
delete info.width; delete info.width;
delete info.height; delete info.height;
delete info.orientation; delete info.orientation;
@@ -267,11 +236,7 @@ describe('FileInfoService', () => {
test('WAV', async () => { test('WAV', async () => {
const path = `${resources}/kick_gaba7.wav`; const path = `${resources}/kick_gaba7.wav`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
delete info.width; delete info.width;
delete info.height; delete info.height;
delete info.orientation; delete info.orientation;
@@ -287,11 +252,7 @@ describe('FileInfoService', () => {
test('AAC', async () => { test('AAC', async () => {
const path = `${resources}/kick_gaba7.aac`; const path = `${resources}/kick_gaba7.aac`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
delete info.width; delete info.width;
delete info.height; delete info.height;
delete info.orientation; delete info.orientation;
@@ -307,11 +268,7 @@ describe('FileInfoService', () => {
test('FLAC', async () => { test('FLAC', async () => {
const path = `${resources}/kick_gaba7.flac`; const path = `${resources}/kick_gaba7.flac`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
delete info.width; delete info.width;
delete info.height; delete info.height;
delete info.orientation; delete info.orientation;
@@ -327,11 +284,7 @@ describe('FileInfoService', () => {
test('MPEG-4 AUDIO (M4A)', async () => { test('MPEG-4 AUDIO (M4A)', async () => {
const path = `${resources}/kick_gaba7.m4a`; const path = `${resources}/kick_gaba7.m4a`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
delete info.width; delete info.width;
delete info.height; delete info.height;
delete info.orientation; delete info.orientation;
@@ -347,11 +300,7 @@ describe('FileInfoService', () => {
test('WEBM AUDIO', async () => { test('WEBM AUDIO', async () => {
const path = `${resources}/kick_gaba7.webm`; const path = `${resources}/kick_gaba7.webm`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
delete info.width; delete info.width;
delete info.height; delete info.height;
delete info.orientation; delete info.orientation;

View File

@@ -18,6 +18,7 @@ import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js'; import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
import { type Response } from 'node-fetch'; import { type Response } from 'node-fetch';
import { ApiError } from "@/server/api/error.js";
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
@@ -48,27 +49,28 @@ export const successfulApiCall = async <E extends keyof misskey.Endpoints, P ext
const res = await api(endpoint, parameters, user); const res = await api(endpoint, parameters, user);
const status = assertion.status ?? (res.body == null ? 204 : 200); const status = assertion.status ?? (res.body == null ? 204 : 200);
assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true })); assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true }));
return res.body;
return res.body as misskey.api.SwitchCaseResponseType<E, P>;
}; };
export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: { export const failedApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status: number, status: number,
code: string, code: string,
id: string id: string
}): Promise<T> => { }): Promise<void> => {
const { endpoint, parameters, user } = request; const { endpoint, parameters, user } = request;
const { status, code, id } = assertion; const { status, code, id } = assertion;
const res = await api(endpoint, parameters, user); const res = await api(endpoint, parameters, user);
assert.strictEqual(res.status, status, inspect(res.body)); assert.strictEqual(res.status, status, inspect(res.body));
assert.strictEqual(res.body.error.code, code, inspect(res.body)); assert.ok(res.body);
assert.strictEqual(res.body.error.id, id, inspect(res.body)); assert.strictEqual(castAsError(res.body as any).error.code, code, inspect(res.body));
return res.body; assert.strictEqual(castAsError(res.body as any).error.id, id, inspect(res.body));
}; };
export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{ export const api = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(path: E, params: P, me?: UserToken): Promise<{
status: number, status: number,
headers: Headers, headers: Headers,
body: any body: misskey.api.SwitchCaseResponseType<E, P>
}> => { }> => {
const bodyAuth: Record<string, string> = {}; const bodyAuth: Record<string, string> = {};
const headers: Record<string, string> = { const headers: Record<string, string> = {
@@ -89,13 +91,14 @@ export const api = async <E extends keyof misskey.Endpoints>(path: E, params: mi
}); });
const body = res.headers.get('content-type') === 'application/json; charset=utf-8' const body = res.headers.get('content-type') === 'application/json; charset=utf-8'
? await res.json() ? await res.json() as misskey.api.SwitchCaseResponseType<E, P>
: null; : null;
return { return {
status: res.status, status: res.status,
headers: res.headers, headers: res.headers,
body, // FIXME: removing this non-null assertion: requires better typing around empty response.
body: body!,
}; };
}; };
@@ -141,7 +144,8 @@ export const post = async (user: UserToken, params: misskey.Endpoints['notes/cre
const res = await api('notes/create', q, user); const res = await api('notes/create', q, user);
return res.body ? res.body.createdNote : null; // FIXME: the return type should reflect this fact.
return (res.body ? res.body.createdNote : null)!;
}; };
export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => { export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => {
@@ -635,3 +639,9 @@ export async function sendEnvResetRequest() {
throw new Error('server env update failed.'); throw new Error('server env update failed.');
} }
} }
// 与えられた値を強制的にエラーとみなす。この関数は型安全性を破壊するため、異常系のアサーション以外で用いられるべきではない。
// FIXME(misskey-js): misskey-jsがエラー情報を公開するようになったらこの関数を廃止する
export function castAsError(obj: Record<string, unknown>): { error: ApiError } {
return obj as { error: ApiError };
}

View File

@@ -29,7 +29,7 @@
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.0.5", "@vitejs/plugin-vue": "5.0.5",
"@vue/compiler-sfc": "3.4.31", "@vue/compiler-sfc": "3.4.31",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
"astring": "1.8.6", "astring": "1.8.6",
"broadcast-channel": "7.0.0", "broadcast-channel": "7.0.0",
"buraha": "0.0.1", "buraha": "0.0.1",

View File

@@ -42,6 +42,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { host } from '@/config.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@@ -63,7 +65,7 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro
const wait = ref(false); const wait = ref(false);
const connection = useStream().useChannel('main'); const connection = useStream().useChannel('main');
if (props.user.isFollowing == null) { if (props.user.isFollowing == null && $i) {
misskeyApi('users/show', { misskeyApi('users/show', {
userId: props.user.id, userId: props.user.id,
}) })
@@ -78,6 +80,8 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
} }
async function onClick() { async function onClick() {
pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` });
wait.value = true; wait.value = true;
try { try {

View File

@@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="['_button', $style.item]" :class="['_button', $style.item]"
:href="item.href" :href="item.href"
:target="item.target" :target="item.target"
:rel="item.target === '_blank' ? 'noopener noreferrer' : undefined"
:download="item.download" :download="item.download"
@click.passive="close(true)" @click.passive="close(true)"
@mouseenter.passive="onItemMouseEnter" @mouseenter.passive="onItemMouseEnter"

View File

@@ -192,7 +192,7 @@ import MkPoll from '@/components/MkPoll.vue';
import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import { pleaseLogin } from '@/scripts/please-login.js'; import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
@@ -214,6 +214,7 @@ import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { shouldCollapsed } from '@/scripts/collapsed.js'; import { shouldCollapsed } from '@/scripts/collapsed.js';
import { host } from '@/config.js';
import { isEnabledUrlPreview } from '@/instance.js'; import { isEnabledUrlPreview } from '@/instance.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { type Keymap } from '@/scripts/hotkey.js'; import { type Keymap } from '@/scripts/hotkey.js';
@@ -298,6 +299,11 @@ const renoteCollapsed = ref(
), ),
); );
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup',
url: `https://${host}/notes/${appearNote.value.id}`,
}));
/* Overload FunctionにLintが対応していないのでコメントアウト /* Overload FunctionにLintが対応していないのでコメントアウト
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean; function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
@@ -431,7 +437,7 @@ if (!props.mock && !inEmbedPage) {
} }
function renote(viaKeyboard = false) { function renote(viaKeyboard = false) {
pleaseLogin(); pleaseLogin(undefined, pleaseLoginContext.value);
showMovedDialog(); showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock }); const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
@@ -441,7 +447,7 @@ function renote(viaKeyboard = false) {
} }
function reply(): void { function reply(): void {
pleaseLogin(); pleaseLogin(undefined, pleaseLoginContext.value);
if (props.mock) { if (props.mock) {
return; return;
} }
@@ -454,7 +460,7 @@ function reply(): void {
} }
function react(): void { function react(): void {
pleaseLogin(); pleaseLogin(undefined, pleaseLoginContext.value);
showMovedDialog(); showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
@@ -585,7 +591,7 @@ function showRenoteMenu(): void {
} }
if (isMyRenote) { if (isMyRenote) {
pleaseLogin(); pleaseLogin(undefined, pleaseLoginContext.value);
os.popupMenu([ os.popupMenu([
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' }, { type: 'divider' },

View File

@@ -209,7 +209,7 @@ import MkPoll from '@/components/MkPoll.vue';
import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import { pleaseLogin } from '@/scripts/please-login.js'; import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
@@ -222,6 +222,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { host } from '@/config.js';
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js'; import { deepClone } from '@/scripts/clone.js';
@@ -296,6 +297,11 @@ const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup',
url: `https://${host}/notes/${appearNote.value.id}`,
}));
const keymap = { const keymap = {
'r': () => reply(), 'r': () => reply(),
'e|a|plus': () => react(), 'e|a|plus': () => react(),
@@ -396,7 +402,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
} }
function renote() { function renote() {
pleaseLogin(); pleaseLogin(undefined, pleaseLoginContext.value);
showMovedDialog(); showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton }); const { menu } = getRenoteMenu({ note: note.value, renoteButton });
@@ -404,7 +410,7 @@ function renote() {
} }
function reply(): void { function reply(): void {
pleaseLogin(); pleaseLogin(undefined, pleaseLoginContext.value);
showMovedDialog(); showMovedDialog();
os.post({ os.post({
reply: appearNote.value, reply: appearNote.value,
@@ -415,7 +421,7 @@ function reply(): void {
} }
function react(): void { function react(): void {
pleaseLogin(); pleaseLogin(undefined, pleaseLoginContext.value);
showMovedDialog(); showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
@@ -499,7 +505,7 @@ async function clip(): Promise<void> {
function showRenoteMenu(): void { function showRenoteMenu(): void {
if (!isMyRenote) return; if (!isMyRenote) return;
pleaseLogin(); pleaseLogin(undefined, pleaseLoginContext.value);
os.popupMenu([{ os.popupMenu([{
text: i18n.ts.unrenote, text: i18n.ts.unrenote,
icon: 'ti ti-trash', icon: 'ti ti-trash',

View File

@@ -34,7 +34,9 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { host } from '@/config.js';
import { useInterval } from '@/scripts/use-interval.js'; import { useInterval } from '@/scripts/use-interval.js';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
const props = defineProps<{ const props = defineProps<{
noteId: string; noteId: string;
@@ -60,6 +62,11 @@ const timer = computed(() => i18n.tsx._poll[
const showResult = ref(props.readOnly || isVoted.value); const showResult = ref(props.readOnly || isVoted.value);
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup',
url: `https://${host}/notes/${props.noteId}`,
}));
// 期限付きアンケート // 期限付きアンケート
if (props.poll.expiresAt) { if (props.poll.expiresAt) {
const tick = () => { const tick = () => {
@@ -76,7 +83,7 @@ if (props.poll.expiresAt) {
} }
const vote = async (id) => { const vote = async (id) => {
pleaseLogin(); pleaseLogin(undefined, pleaseLoginContext.value);
if (props.readOnly || closed.value || isVoted.value) return; if (props.readOnly || closed.value || isVoted.value) return;

View File

@@ -10,6 +10,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="message"> <MkInfo v-if="message">
{{ message }} {{ message }}
</MkInfo> </MkInfo>
<div v-if="openOnRemote" class="_gaps_m">
<div class="_gaps_s">
<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
</MkButton>
<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
{{ i18n.ts.specifyServerHost }}
</button>
</div>
<div :class="$style.orHr">
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
</div>
<div v-if="!totpLogin" class="normal-signin _gaps_m"> <div v-if="!totpLogin" class="normal-signin _gaps_m">
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange"> <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<template #prefix>@</template> <template #prefix>@</template>
@@ -28,8 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.retry }} {{ i18n.ts.retry }}
</MkButton> </MkButton>
</div> </div>
<div v-if="user && user.securityKeys" class="or-hr"> <div v-if="user && user.securityKeys" :class="$style.orHr">
<p class="or-msg">{{ i18n.ts.or }}</p> <p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div> </div>
<div class="twofa-group totp-group _gaps"> <div class="twofa-group totp-group _gaps">
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required> <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
@@ -53,6 +66,7 @@ import { defineAsyncComponent, ref } from 'vue';
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@@ -60,6 +74,7 @@ import MkInfo from '@/components/MkInfo.vue';
import { host as configHost } from '@/config.js'; import { host as configHost } from '@/config.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { query, extractDomain } from '@/scripts/url.js';
import { login } from '@/account.js'; import { login } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@@ -78,22 +93,16 @@ const emit = defineEmits<{
(ev: 'login', v: any): void; (ev: 'login', v: any): void;
}>(); }>();
const props = defineProps({ const props = withDefaults(defineProps<{
withAvatar: { withAvatar?: boolean;
type: Boolean, autoSet?: boolean;
required: false, message?: string,
default: true, openOnRemote?: OpenOnRemoteOptions,
}, }>(), {
autoSet: { withAvatar: true,
type: Boolean, autoSet: false,
required: false, message: '',
default: false, openOnRemote: undefined,
},
message: {
type: String,
required: false,
default: '',
},
}); });
function onUsernameChange(): void { function onUsernameChange(): void {
@@ -222,6 +231,62 @@ function resetPassword(): void {
closed: () => dispose(), closed: () => dispose(),
}); });
} }
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
switch (options.type) {
case 'web':
case 'lookup': {
let _path: string;
if (options.type === 'lookup') {
// TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼
// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
} else {
_path = options.path;
}
if (targetHost) {
window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
}
break;
}
case 'share': {
const params = query(options.params);
if (targetHost) {
window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
} else {
window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
}
break;
}
}
}
async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
const { canceled, result: hostTemp } = await os.inputText({
title: i18n.ts.inputHostName,
placeholder: 'misskey.example.com',
});
if (canceled) return;
let targetHost: string | null = hostTemp;
// ドメイン部分だけを取り出す
targetHost = extractDomain(targetHost);
if (targetHost == null) {
os.alert({
type: 'error',
title: i18n.ts.invalidValue,
text: i18n.ts.tryAgain,
});
return;
}
openRemote(options, targetHost);
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>
@@ -234,4 +299,36 @@ function resetPassword(): void {
background-size: cover; background-size: cover;
border-radius: 100%; border-radius: 100%;
} }
.instanceManualSelectButton {
display: block;
text-align: center;
opacity: .7;
font-size: .8em;
&:hover {
text-decoration: underline;
}
}
.orHr {
position: relative;
margin: .4em auto;
width: 100%;
height: 1px;
background: var(--divider);
}
.orMsg {
position: absolute;
top: -.6em;
display: inline-block;
padding: 0 1em;
background: var(--panel);
font-size: 0.8em;
color: var(--fgOnPanel);
margin: 0;
left: 50%;
transform: translateX(-50%);
}
</style> </style>

View File

@@ -6,21 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkModalWindow <MkModalWindow
ref="dialog" ref="dialog"
:width="370" :width="400"
:height="400" :height="430"
@close="onClose" @close="onClose"
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.ts.login }}</template> <template #header>{{ i18n.ts.login }}</template>
<MkSpacer :marginMin="20" :marginMax="28"> <MkSpacer :marginMin="20" :marginMax="28">
<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/> <MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
</MkSpacer> </MkSpacer>
</MkModalWindow> </MkModalWindow>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef } from 'vue'; import { shallowRef } from 'vue';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import MkSignin from '@/components/MkSignin.vue'; import MkSignin from '@/components/MkSignin.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@@ -28,9 +29,11 @@ import { i18n } from '@/i18n.js';
withDefaults(defineProps<{ withDefaults(defineProps<{
autoSet?: boolean; autoSet?: boolean;
message?: string, message?: string,
openOnRemote?: OpenOnRemoteOptions,
}>(), { }>(), {
autoSet: false, autoSet: false,
message: '', message: '',
openOnRemote: undefined,
}); });
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span> <p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
</div> </div>
</div> </div>
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/> <MkFollowButton v-if="user.id != $i?.id" :class="$style.follow" :user="user" mini/>
</div> </div>
</template> </template>

View File

@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div class="_gaps_s" :class="$style.mainActions"> <div class="_gaps_s" :class="$style.mainActions">
<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton> <MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton> <MkButton :class="$style.mainAction" full rounded link to="https://misskey-hub.net/servers/">{{ i18n.ts.exploreOtherServers }}</MkButton>
<MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton> <MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
</div> </div>
</div> </div>
@@ -65,7 +65,8 @@ import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import MkNumber from '@/components/MkNumber.vue'; import MkNumber from '@/components/MkNumber.vue';
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue'; import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
import { openInstanceMenu } from '@/ui/_common_/common'; import { openInstanceMenu } from '@/ui/_common_/common.js';
import type { MenuItem } from '@/types/menu.js';
const stats = ref<Misskey.entities.StatsResponse | null>(null); const stats = ref<Misskey.entities.StatsResponse | null>(null);
@@ -89,13 +90,9 @@ function signup() {
}); });
} }
function showMenu(ev) { function showMenu(ev: MouseEvent) {
openInstanceMenu(ev); openInstanceMenu(ev);
} }
function exploreOtherServers() {
window.open('https://misskey-hub.net/servers/', '_blank', 'noopener');
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@@ -65,7 +65,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
const validTime = (t: string | boolean | null | undefined) => { const validTime = (t: string | boolean | null | undefined) => {
if (t == null) return null; if (t == null) return null;
if (typeof t === 'boolean') return null; if (typeof t === 'boolean') return null;
return t.match(/^[0-9.]+s$/) ? t : null; return t.match(/^\-?[0-9.]+s$/) ? t : null;
}; };
const validColor = (c: unknown): string | null => { const validColor = (c: unknown): string | null => {

View File

@@ -60,7 +60,7 @@ function onChange({ resolved, key: newKey }) {
if (current == null || 'redirect' in current.route) return; if (current == null || 'redirect' in current.route) return;
currentPageComponent.value = current.route.component; currentPageComponent.value = current.route.component;
currentPageProps.value = current.props; currentPageProps.value = current.props;
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props)); key.value = newKey + JSON.stringify(Object.fromEntries(current.props));
nextTick(() => { nextTick(() => {
// ページ遷移完了後に再びキャッシュを有効化 // ページ遷移完了後に再びキャッシュを有効化

View File

@@ -87,6 +87,7 @@ export const ROLE_POLICIES = [
'canHideAds', 'canHideAds',
'driveCapacityMb', 'driveCapacityMb',
'alwaysMarkNsfw', 'alwaysMarkNsfw',
'canUpdateBioMedia',
'pinLimit', 'pinLimit',
'antennaLimit', 'antennaLimit',
'wordMuteLimit', 'wordMuteLimit',

View File

@@ -23,6 +23,7 @@ import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { embedPage } from '@/config.js'; import { embedPage } from '@/config.js';
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
@@ -684,6 +685,15 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
} }
export function post(props: Record<string, any> = {}): Promise<void> { export function post(props: Record<string, any> = {}): Promise<void> {
pleaseLogin(undefined, (props.initialText || props.initialNote ? {
type: 'share',
params: {
text: props.initialText ?? props.initialNote.text,
visibility: props.initialVisibility ?? props.initialNote?.visibility,
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
},
} : undefined));
showMovedDialog(); showMovedDialog();
return new Promise(resolve => { return new Promise(resolve => {
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない // NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない

View File

@@ -0,0 +1,205 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps_m">
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
<div style="overflow: clip;">
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
<div :class="$style.bannerName">
<b>{{ instance.name ?? host }}</b>
</div>
</div>
</div>
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value><div v-html="instance.description"></div></template>
</MkKeyValue>
<FormSection>
<div class="_gaps_m">
<MkKeyValue :copy="version">
<template #key>Misskey</template>
<template #value>{{ version }}</template>
</MkKeyValue>
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
</div>
<FormLink to="/about-misskey">
<template #icon><i class="ti ti-info-circle"></i></template>
{{ i18n.ts.aboutMisskey }}
</FormLink>
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
<template #icon><i class="ti ti-code"></i></template>
{{ i18n.ts.sourceCode }}
</FormLink>
<MkInfo v-else warn>
{{ i18n.ts.sourceCodeIsNotYetProvided }}
</MkInfo>
</div>
</FormSection>
<FormSection>
<div class="_gaps_m">
<FormSplit>
<MkKeyValue :copy="instance.maintainerName">
<template #key>{{ i18n.ts.administrator }}</template>
<template #value>
<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
<MkKeyValue :copy="instance.maintainerEmail">
<template #key>{{ i18n.ts.contact }}</template>
<template #value>
<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.inquiry }}</template>
<template #value>
<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
</FormSplit>
<div class="_gaps_s">
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
<template #icon><i class="ti ti-user-shield"></i></template>
<template #default>{{ i18n.ts.impressum }}</template>
</FormLink>
<MkFolder v-if="instance.serverRules.length > 0">
<template #icon><i class="ti ti-checkup-list"></i></template>
<template #label>{{ i18n.ts.serverRules }}</template>
<ol class="_gaps_s" :class="$style.rules">
<li v-for="item in instance.serverRules" :key="item" :class="$style.rule">
<div :class="$style.ruleText" v-html="item"></div>
</li>
</ol>
</MkFolder>
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
<template #icon><i class="ti ti-license"></i></template>
<template #default>{{ i18n.ts.termsOfService }}</template>
</FormLink>
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
<template #icon><i class="ti ti-shield-lock"></i></template>
<template #default>{{ i18n.ts.privacyPolicy }}</template>
</FormLink>
<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
<template #icon><i class="ti ti-message"></i></template>
<template #default>{{ i18n.ts.feedback }}</template>
</FormLink>
</div>
</div>
</FormSection>
<FormSuspense v-slot="{ result: stats }" :p="initStats">
<FormSection>
<template #label>{{ i18n.ts.statistics }}</template>
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
</MkKeyValue>
</FormSplit>
</FormSection>
</FormSuspense>
<FormSection>
<template #label>Well-known resources</template>
<div class="_gaps_s">
<FormLink to="/.well-known/host-meta" external>host-meta</FormLink>
<FormLink to="/.well-known/host-meta.json" external>host-meta.json</FormLink>
<FormLink to="/.well-known/nodeinfo" external>nodeinfo</FormLink>
<FormLink to="/robots.txt" external>robots.txt</FormLink>
<FormLink to="/manifest.json" external>manifest.json</FormLink>
</div>
</FormSection>
</div>
</template>
<script lang="ts" setup>
import { host, version } from '@/config.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import number from '@/filters/number.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkLink from '@/components/MkLink.vue';
const initStats = () => misskeyApi('stats', {});
</script>
<style lang="scss" module>
.banner {
text-align: center;
border-radius: 10px;
overflow: clip;
background-color: var(--panel);
background-size: cover;
background-position: center center;
}
.bannerIcon {
display: block;
margin: 16px auto 0 auto;
height: 64px;
border-radius: 8px;
}
.bannerName {
display: block;
padding: 16px;
color: #fff;
text-shadow: 0 0 8px #000;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
}
.rules {
counter-reset: item;
list-style: none;
padding: 0;
margin: 0;
}
.rule {
display: flex;
gap: 8px;
word-break: break-word;
&::before {
flex-shrink: 0;
display: flex;
position: sticky;
top: calc(var(--stickyTop, 0px) + 8px);
counter-increment: item;
content: counter(item);
width: 32px;
height: 32px;
line-height: 32px;
background-color: var(--accentedBg);
color: var(--accent);
font-size: 13px;
font-weight: bold;
align-items: center;
justify-content: center;
border-radius: 999px;
}
}
.ruleText {
padding-top: 6px;
}
</style>

View File

@@ -8,113 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20"> <MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
<div class="_gaps_m"> <XOverview/>
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
<div style="overflow: clip;">
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
<div :class="$style.bannerName">
<b>{{ instance.name ?? host }}</b>
</div>
</div>
</div>
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value><div v-html="instance.description"></div></template>
</MkKeyValue>
<FormSection>
<div class="_gaps_m">
<MkKeyValue :copy="version">
<template #key>Misskey</template>
<template #value>{{ version }}</template>
</MkKeyValue>
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
</div>
<FormLink to="/about-misskey">
<template #icon><i class="ti ti-info-circle"></i></template>
{{ i18n.ts.aboutMisskey }}
</FormLink>
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
<template #icon><i class="ti ti-code"></i></template>
{{ i18n.ts.sourceCode }}
</FormLink>
<MkInfo v-else warn>
{{ i18n.ts.sourceCodeIsNotYetProvided }}
</MkInfo>
</div>
</FormSection>
<FormSection>
<div class="_gaps_m">
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.administrator }}</template>
<template #value>{{ instance.maintainerName }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.contact }}</template>
<template #value>{{ instance.maintainerEmail }}</template>
</MkKeyValue>
</FormSplit>
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
<template #icon><i class="ti ti-user-shield"></i></template>
{{ i18n.ts.impressum }}
</FormLink>
<div class="_gaps_s">
<MkFolder v-if="instance.serverRules.length > 0">
<template #label>
<i class="ti ti-checkup-list"></i>
{{ i18n.ts.serverRules }}
</template>
<ol class="_gaps_s" :class="$style.rules">
<li v-for="(item, index) in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
</ol>
</MkFolder>
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
<template #icon><i class="ti ti-license"></i></template>
{{ i18n.ts.termsOfService }}
</FormLink>
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
<template #icon><i class="ti ti-shield-lock"></i></template>
{{ i18n.ts.privacyPolicy }}
</FormLink>
<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
<template #icon><i class="ti ti-message"></i></template>
{{ i18n.ts.feedback }}
</FormLink>
</div>
</div>
</FormSection>
<FormSuspense :p="initStats">
<FormSection>
<template #label>{{ i18n.ts.statistics }}</template>
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
</MkKeyValue>
</FormSplit>
</FormSection>
</FormSuspense>
<FormSection>
<template #label>Well-known resources</template>
<div class="_gaps_s">
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
</div>
</FormSection>
</div>
</MkSpacer> </MkSpacer>
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> <MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
<XEmojis/> <XEmojis/>
@@ -130,26 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch, ref } from 'vue'; import { computed, defineAsyncComponent, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import XEmojis from './about.emojis.vue';
import XFederation from './about.federation.vue';
import { version, host } from '@/config.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkInstanceStats from '@/components/MkInstanceStats.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import number from '@/filters/number.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { instance } from '@/instance.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue'));
const XFederation = defineAsyncComponent(() => import('@/pages/about.federation.vue'));
const MkInstanceStats = defineAsyncComponent(() => import('@/components/MkInstanceStats.vue'));
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
initialTab?: string; initialTab?: string;
@@ -157,7 +41,6 @@ const props = withDefaults(defineProps<{
initialTab: 'overview', initialTab: 'overview',
}); });
const stats = ref<Misskey.entities.StatsResponse | null>(null);
const tab = ref(props.initialTab); const tab = ref(props.initialTab);
watch(tab, () => { watch(tab, () => {
@@ -166,11 +49,6 @@ watch(tab, () => {
} }
}); });
const initStats = () => misskeyApi('stats', {
}).then((res) => {
stats.value = res;
});
const headerActions = computed(() => []); const headerActions = computed(() => []);
const headerTabs = computed(() => [{ const headerTabs = computed(() => [{
@@ -195,64 +73,3 @@ definePageMetadata(() => ({
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',
})); }));
</script> </script>
<style lang="scss" module>
.banner {
text-align: center;
border-radius: 10px;
overflow: clip;
background-size: cover;
background-position: center center;
}
.bannerIcon {
display: block;
margin: 16px auto 0 auto;
height: 64px;
border-radius: 8px;
}
.bannerName {
display: block;
padding: 16px;
color: #fff;
text-shadow: 0 0 8px #000;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
}
.rules {
counter-reset: item;
list-style: none;
padding: 0;
margin: 0;
}
.rule {
display: flex;
gap: 8px;
word-break: break-word;
&::before {
flex-shrink: 0;
display: flex;
position: sticky;
top: calc(var(--stickyTop, 0px) + 8px);
counter-increment: item;
content: counter(item);
width: 32px;
height: 32px;
line-height: 32px;
background-color: var(--accentedBg);
color: var(--accent);
font-size: 13px;
font-weight: bold;
align-items: center;
justify-content: center;
border-radius: 999px;
}
}
.ruleText {
padding-top: 6px;
}
</style>

View File

@@ -378,6 +378,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
<template #suffix>
<span v-if="role.policies.canUpdateBioMedia.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canUpdateBioMedia.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateBioMedia)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canUpdateBioMedia.value" :disabled="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canUpdateBioMedia.priority" :min="0" :max="2" :step="1" easing :textConverter="(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.pinMax, 'pinLimit'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
<template #label>{{ i18n.ts._role._options.pinMax }}</template> <template #label>{{ i18n.ts._role._options.pinMax }}</template>
<template #suffix> <template #suffix>

View File

@@ -134,6 +134,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch> </MkSwitch>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
<template #suffix>{{ policies.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canUpdateBioMedia">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
<template #label>{{ i18n.ts._role._options.pinMax }}</template> <template #label>{{ i18n.ts._role._options.pinMax }}</template>
<template #suffix>{{ policies.pinLimit }}</template> <template #suffix>{{ policies.pinLimit }}</template>

View File

@@ -7,18 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader/></template> <template #header><MkPageHeader/></template>
<MkSpacer :contentMax="600" :marginMin="20"> <MkSpacer :contentMax="600" :marginMin="20">
<div class="_gaps"> <div class="_gaps_m">
<MkKeyValue> <MkKeyValue :copy="instance.maintainerName">
<template #key>{{ i18n.ts.inquiry }}</template> <template #key>{{ i18n.ts.administrator }}</template>
<template #value> <template #value>
<MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink> <template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template> </template>
</MkKeyValue> </MkKeyValue>
<MkKeyValue :copy="instance.maintainerEmail">
<MkKeyValue> <template #key>{{ i18n.ts.contact }}</template>
<template #key>{{ i18n.ts.email }}</template>
<template #value> <template #value>
<div>{{ instance.maintainerEmail }}</div> <template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template>
</MkKeyValue>
<MkKeyValue :copy="instance.inquiryUrl">
<template #key>{{ i18n.ts.inquiry }}</template>
<template #value>
<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
</template> </template>
</MkKeyValue> </MkKeyValue>
</div> </div>
@@ -28,8 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
import MkLink from '@/components/MkLink.vue'; import MkLink from '@/components/MkLink.vue';

View File

@@ -79,6 +79,7 @@ import { defaultStore } from '@/store.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js'; import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { pleaseLogin } from '@/scripts/please-login.js';
const props = defineProps<{ const props = defineProps<{
id: string; id: string;
@@ -143,6 +144,7 @@ function shareWithNote() {
function like() { function like() {
if (!flash.value) return; if (!flash.value) return;
pleaseLogin();
os.apiWithDialog('flash/like', { os.apiWithDialog('flash/like', {
flashId: flash.value.id, flashId: flash.value.id,
@@ -154,6 +156,7 @@ function like() {
async function unlike() { async function unlike() {
if (!flash.value) return; if (!flash.value) return;
pleaseLogin();
const confirm = await os.confirm({ const confirm = await os.confirm({
type: 'warning', type: 'warning',

View File

@@ -1,71 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { mainRouter } from '@/router/main.js';
async function follow(user): Promise<void> {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.followConfirm({ name: user.name || user.username }),
});
if (canceled) {
window.close();
return;
}
os.apiWithDialog('following/create', {
userId: user.id,
withReplies: defaultStore.state.defaultWithReplies,
});
user.withReplies = defaultStore.state.defaultWithReplies;
}
const acct = new URL(location.href).searchParams.get('acct');
if (acct == null) {
throw new Error('acct required');
}
let promise;
if (acct.startsWith('https://')) {
promise = misskeyApi('ap/show', {
uri: acct,
});
promise.then(res => {
if (res.type === 'User') {
follow(res.object);
} else if (res.type === 'Note') {
mainRouter.push(`/notes/${res.object.id}`);
} else {
os.alert({
type: 'error',
text: 'Not a user',
}).then(() => {
window.close();
});
}
});
} else {
promise = misskeyApi('users/show', Misskey.acct.parse(acct));
promise.then(user => {
follow(user);
});
}
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
</script>

View File

@@ -0,0 +1,97 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
<div v-if="state === 'done'" class="_buttonsCenter">
<MkButton @click="close">{{ i18n.ts.close }}</MkButton>
<MkButton @click="goToMisskey">{{ i18n.ts.goToMisskey }}</MkButton>
</div>
<div v-else class="_fullInfo">
<MkLoading/>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { mainRouter } from '@/router/main.js';
import MkButton from '@/components/MkButton.vue';
const state = ref<'fetching' | 'done'>('fetching');
function fetch() {
const params = new URL(location.href).searchParams;
// acctのほうはdeprecated
let uri = params.get('uri') ?? params.get('acct');
if (uri == null) {
state.value = 'done';
return;
}
let promise: Promise<any>;
if (uri.startsWith('https://')) {
promise = misskeyApi('ap/show', {
uri,
});
promise.then(res => {
if (res.type === 'User') {
mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`);
} else if (res.type === 'Note') {
mainRouter.replace(`/notes/${res.object.id}`);
} else {
os.alert({
type: 'error',
text: 'Not a user',
});
}
});
} else {
if (uri.startsWith('acct:')) {
uri = uri.slice(5);
}
promise = misskeyApi('users/show', Misskey.acct.parse(uri));
promise.then(user => {
mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`);
});
}
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
}
function close(): void {
window.close();
// 閉じなければ100ms後タイムラインに
window.setTimeout(() => {
location.href = '/';
}, 100);
}
function goToMisskey(): void {
location.href = '/';
}
fetch();
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata({
title: i18n.ts.lookup,
icon: 'ti ti-world-search',
});
</script>

View File

@@ -32,9 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span> <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
<div v-if="$i" class="actions"> <div class="actions">
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button> <button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
<MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> <MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div> </div>
</div> </div>
<MkAvatar class="avatar" :user="user" indicator/> <MkAvatar class="avatar" :user="user" indicator/>

View File

@@ -237,8 +237,18 @@ const routes: RouteDef[] = [{
origin: 'origin', origin: 'origin',
}, },
}, { }, {
// Legacy Compatibility
path: '/authorize-follow', path: '/authorize-follow',
component: page(() => import('@/pages/follow.vue')), redirect: '/lookup',
loginRequired: true,
}, {
// Mastodon Compatibility
path: '/authorize_interaction',
redirect: '/lookup',
loginRequired: true,
}, {
path: '/lookup',
component: page(() => import('@/pages/lookup.vue')),
loginRequired: true, loginRequired: true,
}, { }, {
path: '/share', path: '/share',

View File

@@ -197,7 +197,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
copyToClipboard(`${url}/${canonical}`); copyToClipboard(`${url}/${canonical}`);
}, },
}, { }, ...($i ? [{
icon: 'ti ti-mail', icon: 'ti ti-mail',
text: i18n.ts.sendMessage, text: i18n.ts.sendMessage,
action: () => { action: () => {
@@ -270,7 +270,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
}, },
})); }));
}, },
}] as any; }] : [])] as any;
if ($i && meId !== user.id) { if ($i && meId !== user.id) {
if (iAmModerator) { if (iAmModerator) {

View File

@@ -114,6 +114,7 @@ const matchPatterns = (ev: KeyboardEvent, action: Action) => {
const key = ev.key.toLowerCase(); const key = ev.key.toLowerCase();
return patterns.some(({ which, ctrl, shift, alt }) => { return patterns.some(({ which, ctrl, shift, alt }) => {
if ( if (
options.allowRepeat === false &&
latestHotkey != null && latestHotkey != null &&
latestHotkey.which.includes(key) && latestHotkey.which.includes(key) &&
latestHotkey.ctrl === ctrl && latestHotkey.ctrl === ctrl &&

View File

@@ -8,12 +8,49 @@ import { $i } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { popup } from '@/os.js'; import { popup } from '@/os.js';
export function pleaseLogin(path?: string) { export type OpenOnRemoteOptions = {
/**
* 外部のMisskey Webで特定のパスを開く
*/
type: 'web';
/**
* 内部パス(例: `/settings`
*/
path: string;
} | {
/**
* 外部のMisskey Webで照会する
*/
type: 'lookup';
/**
* 照会したいエンティティのURL
*
* (例: `https://misskey.example.com/notes/abcdexxxxyz`
*/
url: string;
} | {
/**
* 外部のMisskeyでートする
*/
type: 'share';
/**
* `/share` ページに渡すクエリストリング
*
* @see https://go.misskey-hub.net/spec/share/
*/
params: Record<string, string>;
};
export function pleaseLogin(path?: string, openOnRemote?: OpenOnRemoteOptions) {
if ($i) return; if ($i) return;
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
autoSet: true, autoSet: true,
message: i18n.ts.signinRequired, message: openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired,
openOnRemote,
}, { }, {
cancelled: () => { cancelled: () => {
if (path) { if (path) {

View File

@@ -21,3 +21,8 @@ export function query(obj: Record<string, any>): string {
export function appendQuery(url: string, query: string): string { export function appendQuery(url: string, query: string): string {
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
} }
export function extractDomain(url: string) {
const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im);
return match ? match[1] : null;
}

View File

@@ -85,29 +85,29 @@ export function openInstanceMenu(ev: MouseEvent) {
icon: 'ti ti-help-circle', icon: 'ti ti-help-circle',
to: '/contact', to: '/contact',
}, (instance.impressumUrl) ? { }, (instance.impressumUrl) ? {
type: 'a',
text: i18n.ts.impressum, text: i18n.ts.impressum,
icon: 'ti ti-file-invoice', icon: 'ti ti-file-invoice',
action: () => { href: instance.impressumUrl,
window.open(instance.impressumUrl, '_blank', 'noopener'); target: '_blank',
},
} : undefined, (instance.tosUrl) ? { } : undefined, (instance.tosUrl) ? {
type: 'a',
text: i18n.ts.termsOfService, text: i18n.ts.termsOfService,
icon: 'ti ti-notebook', icon: 'ti ti-notebook',
action: () => { href: instance.tosUrl,
window.open(instance.tosUrl, '_blank', 'noopener'); target: '_blank',
},
} : undefined, (instance.privacyPolicyUrl) ? { } : undefined, (instance.privacyPolicyUrl) ? {
type: 'a',
text: i18n.ts.privacyPolicy, text: i18n.ts.privacyPolicy,
icon: 'ti ti-shield-lock', icon: 'ti ti-shield-lock',
action: () => { href: instance.privacyPolicyUrl,
window.open(instance.privacyPolicyUrl, '_blank', 'noopener'); target: '_blank',
},
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, { } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
type: 'a',
text: i18n.ts.document, text: i18n.ts.document,
icon: 'ti ti-bulb', icon: 'ti ti-bulb',
action: () => { href: 'https://misskey-hub.net/docs/for-users/',
window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener'); target: '_blank',
},
}, ($i) ? { }, ($i) ? {
text: i18n.ts._initialTutorial.launchTutorial, text: i18n.ts._initialTutorial.launchTutorial,
icon: 'ti ti-presentation', icon: 'ti ti-presentation',

View File

@@ -95,7 +95,6 @@ async function watchSrc() {
process.on('SIGHUP', resolve); process.on('SIGHUP', resolve);
process.on('SIGINT', resolve); process.on('SIGINT', resolve);
process.on('SIGTERM', resolve); process.on('SIGTERM', resolve);
process.on('SIGKILL', resolve);
process.on('uncaughtException', reject); process.on('uncaughtException', reject);
process.on('exit', resolve); process.on('exit', resolve);
}).finally(async () => { }).finally(async () => {

View File

@@ -95,7 +95,6 @@ async function watchSrc() {
process.on('SIGHUP', resolve); process.on('SIGHUP', resolve);
process.on('SIGINT', resolve); process.on('SIGINT', resolve);
process.on('SIGTERM', resolve); process.on('SIGTERM', resolve);
process.on('SIGKILL', resolve);
process.on('uncaughtException', reject); process.on('uncaughtException', reject);
process.on('exit', resolve); process.on('exit', resolve);
}).finally(async () => { }).finally(async () => {

View File

@@ -4786,6 +4786,7 @@ export type components = {
canHideAds: boolean; canHideAds: boolean;
driveCapacityMb: number; driveCapacityMb: number;
alwaysMarkNsfw: boolean; alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean;
pinLimit: number; pinLimit: number;
antennaLimit: number; antennaLimit: number;
wordMuteLimit: number; wordMuteLimit: number;

View File

@@ -318,8 +318,10 @@ export abstract class Connection<Channel extends AnyOf<Channels> = any> extends
this.stream = stream; this.stream = stream;
this.channel = channel; this.channel = channel;
if (name !== undefined) {
this.name = name; this.name = name;
} }
}
public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void { public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void {
this.stream.send('ch', { this.stream.send('ch', {

View File

@@ -15,6 +15,7 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"esModuleInterop": true, "esModuleInterop": true,
"exactOptionalPropertyTypes": true,
"typeRoots": [ "typeRoots": [
"./node_modules/@types" "./node_modules/@types"
], ],

View File

@@ -95,7 +95,6 @@ async function watchSrc() {
process.on('SIGHUP', resolve); process.on('SIGHUP', resolve);
process.on('SIGINT', resolve); process.on('SIGINT', resolve);
process.on('SIGTERM', resolve); process.on('SIGTERM', resolve);
process.on('SIGKILL', resolve);
process.on('uncaughtException', reject); process.on('uncaughtException', reject);
process.on('exit', resolve); process.on('exit', resolve);
}).finally(async () => { }).finally(async () => {

View File

@@ -2,7 +2,6 @@
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"noEmitOnError": false, "noEmitOnError": false,
"noImplicitAny": false,
"noImplicitReturns": true, "noImplicitReturns": true,
"noUnusedParameters": false, "noUnusedParameters": false,
"noUnusedLocals": true, "noUnusedLocals": true,

50
pnpm-lock.yaml generated
View File

@@ -725,8 +725,8 @@ importers:
specifier: 3.4.31 specifier: 3.4.31
version: 3.4.31 version: 3.4.31
aiscript-vscode: aiscript-vscode:
specifier: github:aiscript-dev/aiscript-vscode#v0.1.9 specifier: github:aiscript-dev/aiscript-vscode#v0.1.11
version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02 version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9
astring: astring:
specifier: 1.8.6 specifier: 1.8.6
version: 1.8.6 version: 1.8.6
@@ -919,7 +919,7 @@ importers:
version: 8.1.11(bufferutil@4.0.7)(encoding@0.1.13)(prettier@3.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))(vue@3.4.31(typescript@5.5.3)) version: 8.1.11(bufferutil@4.0.7)(encoding@0.1.13)(prettier@3.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))(vue@3.4.31(typescript@5.5.3))
'@testing-library/vue': '@testing-library/vue':
specifier: 8.1.0 specifier: 8.1.0
version: 8.1.0(@vue/compiler-sfc@3.4.31)(@vue/server-renderer@3.4.29(vue@3.4.31(typescript@5.5.3)))(vue@3.4.31(typescript@5.5.3)) version: 8.1.0(@vue/compiler-sfc@3.4.31)(@vue/server-renderer@3.4.31(vue@3.4.31(typescript@5.5.3)))(vue@3.4.31(typescript@5.5.3))
'@types/escape-regexp': '@types/escape-regexp':
specifier: 0.0.3 specifier: 0.0.3
version: 0.0.3 version: 0.0.3
@@ -5438,9 +5438,6 @@ packages:
'@vue/compiler-sfc@3.4.31': '@vue/compiler-sfc@3.4.31':
resolution: {integrity: sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==} resolution: {integrity: sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==}
'@vue/compiler-ssr@3.4.29':
resolution: {integrity: sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==}
'@vue/compiler-ssr@3.4.31': '@vue/compiler-ssr@3.4.31':
resolution: {integrity: sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==} resolution: {integrity: sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==}
@@ -5472,11 +5469,6 @@ packages:
'@vue/runtime-dom@3.4.31': '@vue/runtime-dom@3.4.31':
resolution: {integrity: sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==} resolution: {integrity: sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==}
'@vue/server-renderer@3.4.29':
resolution: {integrity: sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==}
peerDependencies:
vue: 3.4.29
'@vue/server-renderer@3.4.31': '@vue/server-renderer@3.4.31':
resolution: {integrity: sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==} resolution: {integrity: sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==}
peerDependencies: peerDependencies:
@@ -5593,9 +5585,9 @@ packages:
resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==} resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==}
engines: {node: '>=18'} engines: {node: '>=18'}
aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02: aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9:
resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02} resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9}
version: 0.1.9 version: 0.1.11
engines: {vscode: ^1.83.0} engines: {vscode: ^1.83.0}
ajv-draft-04@1.0.0: ajv-draft-04@1.0.0:
@@ -11716,6 +11708,9 @@ packages:
vue-component-type-helpers@2.0.24: vue-component-type-helpers@2.0.24:
resolution: {integrity: sha512-Jr5N8QVYEcbQuMN1LRgvg61758G8HTnzUlQsAFOxx6Y6X8kmhJ7C+jOvWsQruYxi3uHhhS6BghyRlyiwO99DBg==} resolution: {integrity: sha512-Jr5N8QVYEcbQuMN1LRgvg61758G8HTnzUlQsAFOxx6Y6X8kmhJ7C+jOvWsQruYxi3uHhhS6BghyRlyiwO99DBg==}
vue-component-type-helpers@2.0.26:
resolution: {integrity: sha512-sO9qQ8oC520SW6kqlls0iqDak53gsTVSrYylajgjmkt1c0vcgjsGSy1KzlDrbEx8pm02IEYhlUkU5hCYf8rwtg==}
vue-demi@0.14.7: vue-demi@0.14.7:
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -16525,7 +16520,7 @@ snapshots:
ts-dedent: 2.2.0 ts-dedent: 2.2.0
type-fest: 2.19.0 type-fest: 2.19.0
vue: 3.4.31(typescript@5.5.3) vue: 3.4.31(typescript@5.5.3)
vue-component-type-helpers: 2.0.24 vue-component-type-helpers: 2.0.26
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- prettier - prettier
@@ -16789,11 +16784,11 @@ snapshots:
dependencies: dependencies:
'@testing-library/dom': 10.1.0 '@testing-library/dom': 10.1.0
'@testing-library/vue@8.1.0(@vue/compiler-sfc@3.4.31)(@vue/server-renderer@3.4.29(vue@3.4.31(typescript@5.5.3)))(vue@3.4.31(typescript@5.5.3))': '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.4.31)(@vue/server-renderer@3.4.31(vue@3.4.31(typescript@5.5.3)))(vue@3.4.31(typescript@5.5.3))':
dependencies: dependencies:
'@babel/runtime': 7.23.4 '@babel/runtime': 7.23.4
'@testing-library/dom': 9.3.4 '@testing-library/dom': 9.3.4
'@vue/test-utils': 2.4.1(@vue/server-renderer@3.4.29(vue@3.4.31(typescript@5.5.3)))(vue@3.4.31(typescript@5.5.3)) '@vue/test-utils': 2.4.1(@vue/server-renderer@3.4.31(vue@3.4.31(typescript@5.5.3)))(vue@3.4.31(typescript@5.5.3))
vue: 3.4.31(typescript@5.5.3) vue: 3.4.31(typescript@5.5.3)
optionalDependencies: optionalDependencies:
'@vue/compiler-sfc': 3.4.31 '@vue/compiler-sfc': 3.4.31
@@ -17599,12 +17594,6 @@ snapshots:
postcss: 8.4.38 postcss: 8.4.38
source-map-js: 1.2.0 source-map-js: 1.2.0
'@vue/compiler-ssr@3.4.29':
dependencies:
'@vue/compiler-dom': 3.4.29
'@vue/shared': 3.4.29
optional: true
'@vue/compiler-ssr@3.4.31': '@vue/compiler-ssr@3.4.31':
dependencies: dependencies:
'@vue/compiler-dom': 3.4.31 '@vue/compiler-dom': 3.4.31
@@ -17653,13 +17642,6 @@ snapshots:
'@vue/shared': 3.4.31 '@vue/shared': 3.4.31
csstype: 3.1.3 csstype: 3.1.3
'@vue/server-renderer@3.4.29(vue@3.4.31(typescript@5.5.3))':
dependencies:
'@vue/compiler-ssr': 3.4.29
'@vue/shared': 3.4.29
vue: 3.4.31(typescript@5.5.3)
optional: true
'@vue/server-renderer@3.4.31(vue@3.4.31(typescript@5.5.3))': '@vue/server-renderer@3.4.31(vue@3.4.31(typescript@5.5.3))':
dependencies: dependencies:
'@vue/compiler-ssr': 3.4.31 '@vue/compiler-ssr': 3.4.31
@@ -17670,13 +17652,13 @@ snapshots:
'@vue/shared@3.4.31': {} '@vue/shared@3.4.31': {}
'@vue/test-utils@2.4.1(@vue/server-renderer@3.4.29(vue@3.4.31(typescript@5.5.3)))(vue@3.4.31(typescript@5.5.3))': '@vue/test-utils@2.4.1(@vue/server-renderer@3.4.31(vue@3.4.31(typescript@5.5.3)))(vue@3.4.31(typescript@5.5.3))':
dependencies: dependencies:
js-beautify: 1.14.9 js-beautify: 1.14.9
vue: 3.4.31(typescript@5.5.3) vue: 3.4.31(typescript@5.5.3)
vue-component-type-helpers: 1.8.4 vue-component-type-helpers: 1.8.4
optionalDependencies: optionalDependencies:
'@vue/server-renderer': 3.4.29(vue@3.4.31(typescript@5.5.3)) '@vue/server-renderer': 3.4.31(vue@3.4.31(typescript@5.5.3))
'@webgpu/types@0.1.30': {} '@webgpu/types@0.1.30': {}
@@ -17767,7 +17749,7 @@ snapshots:
clean-stack: 5.2.0 clean-stack: 5.2.0
indent-string: 5.0.0 indent-string: 5.0.0
aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02: aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9:
dependencies: dependencies:
'@aiscript-dev/aiscript-languageserver': https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz '@aiscript-dev/aiscript-languageserver': https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz
vscode-languageclient: 9.0.1 vscode-languageclient: 9.0.1
@@ -24875,6 +24857,8 @@ snapshots:
vue-component-type-helpers@2.0.24: {} vue-component-type-helpers@2.0.24: {}
vue-component-type-helpers@2.0.26: {}
vue-demi@0.14.7(vue@3.4.31(typescript@5.5.3)): vue-demi@0.14.7(vue@3.4.31(typescript@5.5.3)):
dependencies: dependencies:
vue: 3.4.31(typescript@5.5.3) vue: 3.4.31(typescript@5.5.3)