diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index 3907615f73a4..e75e32a17a87 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -220,5 +220,10 @@ allowedPrivateNetworks: [ '127.0.0.1/32' ] +# Disable automatic redirect for ActivityPub object lookup. (default: false) +# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. +# However it will make it impossible for other instances to lookup third-party user and notes through your URL. +#disallowExternalApRedirect: true + # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.config/docker_example.yml b/.config/docker_example.yml index ad9ae4fd9adf..1ffed00cc71d 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -235,6 +235,11 @@ signToActivityPubGet: true # '127.0.0.1/32' #] +# Disable automatic redirect for ActivityPub object lookup. (default: false) +# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. +# However it will make it impossible for other instances to lookup third-party user and notes through your URL. +#disallowExternalApRedirect: true + # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.config/example.yml b/.config/example.yml index 349c2e9730e6..71427c84bc89 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -334,6 +334,11 @@ signToActivityPubGet: true # '127.0.0.1/32' #] +# Disable automatic redirect for ActivityPub object lookup. (default: false) +# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. +# However it will make it impossible for other instances to lookup third-party user and notes through your URL. +#disallowExternalApRedirect: true + # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 713c2e5fdd8f..8dd9d1c70424 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,9 @@ "ghcr.io/devcontainers/features/node:1": { "version": "22.11.0" }, - "ghcr.io/devcontainers-contrib/features/corepack:1": {} + "ghcr.io/devcontainers-extra/features/corepack:1": { + "version": "0.31.0" + } }, "forwardPorts": [3000], "postCreateCommand": "/bin/bash .devcontainer/init.sh", diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d4678ec5e042..b1c256225dec 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,7 +9,7 @@ updates: directory: "/" schedule: interval: daily - open-pull-requests-limit: 100 + open-pull-requests-limit: 0 # Add only the root, not each workspace item # https://github.com/dependabot/dependabot-core/issues/4993#issuecomment-1289133027 @@ -17,7 +17,7 @@ updates: directory: "/" schedule: interval: daily - open-pull-requests-limit: 10 + open-pull-requests-limit: 0 # List dependencies required to be updated together, sharing the same version numbers. # Those who simply have the common owner (e.g. @fastify) don't need to be listed. groups: diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index 6f4219b54b32..fdd128be33f5 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -20,12 +20,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 - run: corepack enable - name: Setup Node.js - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index fdca621cfc92..52acbfebeb09 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -12,9 +12,9 @@ jobs: steps: - name: Checkout head - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 - name: Setup Node.js - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml index bb155419417e..50a8c3ab34f8 100644 --- a/.github/workflows/check-misskey-js-autogen.yml +++ b/.github/workflows/check-misskey-js-autogen.yml @@ -18,7 +18,7 @@ jobs: if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} steps: - name: checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 with: submodules: true persist-credentials: false @@ -29,7 +29,7 @@ jobs: - name: setup node id: setup-node - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: pnpm @@ -66,7 +66,7 @@ jobs: if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} steps: - name: checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 with: submodules: true persist-credentials: false diff --git a/.github/workflows/check-misskey-js-version.yml b/.github/workflows/check-misskey-js-version.yml index 99c29ac974cf..2b15cbee535f 100644 --- a/.github/workflows/check-misskey-js-version.yml +++ b/.github/workflows/check-misskey-js-version.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 - name: Check version run: | if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml index 05582008b5f2..bc6be308d1ac 100644 --- a/.github/workflows/check-spdx-license-id.yml +++ b/.github/workflows/check-spdx-license-id.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 - name: Check run: | counter=0 diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml index 03dfcd0a0b21..eaf922d4bc48 100644 --- a/.github/workflows/check_copyright_year.yml +++ b/.github/workflows/check_copyright_year.yml @@ -10,7 +10,7 @@ jobs: check_copyright_year: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 - run: | if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then echo "Please change copyright year!" diff --git a/.github/workflows/deploy-test-environment.yml b/.github/workflows/deploy-test-environment.yml index 66b15beb91ea..46baf7421bad 100644 --- a/.github/workflows/deploy-test-environment.yml +++ b/.github/workflows/deploy-test-environment.yml @@ -28,7 +28,7 @@ jobs: wait_time: ${{ steps.get-wait-time.outputs.wait_time }} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 - name: Check allowed users id: check-allowed-users diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index ac2b1b4d358a..56dedf273d2f 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -27,7 +27,7 @@ jobs: platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index db899ba386be..eb98273ba0ea 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -32,7 +32,7 @@ jobs: platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Docker meta diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index c3dba4213d31..305460791375 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -15,7 +15,7 @@ jobs: DOCKER_CONTENT_TRUST: 1 DOCKLE_VERSION: 0.4.14 steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 - name: Download and install dockle v${{ env.DOCKLE_VERSION }} run: | curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb" diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index 6805e8bc3c5c..2da96474607e 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -30,14 +30,14 @@ jobs: ref: refs/pull/${{ github.event.number }}/merge steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: ref: ${{ matrix.ref }} submodules: true - name: Install pnpm uses: pnpm/action-setup@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2f6938d2e43b..a34ac38d7498 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,12 +36,12 @@ jobs: pnpm_install: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: fetch-depth: 0 submodules: true - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4.1.0 + - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' @@ -67,19 +67,19 @@ jobs: eslint-cache-version: v1 eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: fetch-depth: 0 submodules: true - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4.1.0 + - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' - run: corepack enable - run: pnpm i --frozen-lockfile - name: Restore eslint cache - uses: actions/cache@v4.2.0 + uses: actions/cache@v4.2.1 with: path: ${{ env.eslint-cache-path }} key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} @@ -97,12 +97,12 @@ jobs: - sw - misskey-js steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: fetch-depth: 0 submodules: true - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4.1.0 + - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml index 95d29bf828c6..2daeaa3bd79a 100644 --- a/.github/workflows/locale.yml +++ b/.github/workflows/locale.yml @@ -18,12 +18,12 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: fetch-depth: 0 submodules: true - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4.1.0 + - uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml deleted file mode 100644 index 8362c006eba4..000000000000 --- a/.github/workflows/ok-to-test.yml +++ /dev/null @@ -1,36 +0,0 @@ -# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event -name: Ok To Test - -on: - issue_comment: - types: [created] - -jobs: - ok-to-test: - runs-on: ubuntu-latest - # Only run for PRs, not issue comments - if: ${{ github.event.issue.pull_request }} - steps: - # Generate a GitHub App installation access token from an App ID and private key - # To create a new GitHub App: - # https://developer.github.com/apps/building-github-apps/creating-a-github-app/ - # See app.yml for an example app manifest - - name: Generate token - id: generate_token - uses: tibdex/github-app-token@v2 - with: - app_id: ${{ secrets.DEPLOYBOT_APP_ID }} - private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }} - - - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v4 - env: - TOKEN: ${{ steps.generate_token.outputs.token }} - with: - token: ${{ env.TOKEN }} # GitHub App installation access token - # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work - reaction-token: ${{ secrets.GITHUB_TOKEN }} - issue-type: pull-request - commands: deploy - named-args: true - permission: write diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml index fc224a62395b..8e4ad4368b98 100644 --- a/.github/workflows/on-release-created.yml +++ b/.github/workflows/on-release-created.yml @@ -23,13 +23,13 @@ jobs: node-version: [22.11.0] steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: submodules: true - name: Install pnpm uses: pnpm/action-setup@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml deleted file mode 100644 index 964d24c3d7b9..000000000000 --- a/.github/workflows/pr-preview-deploy.yml +++ /dev/null @@ -1,92 +0,0 @@ -# Run secret-dependent integration tests only after /deploy approval -on: - repository_dispatch: - types: [deploy-command] - -name: Deploy preview environment - -jobs: - # Repo owner has commented /deploy on a (fork-based) pull request - deploy-preview-environment: - runs-on: ubuntu-latest - if: - github.event.client_payload.slash_command.sha != '' && - contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) - steps: - - uses: actions/github-script@v7.0.1 - id: check-id - env: - number: ${{ github.event.client_payload.pull_request.number }} - job: ${{ github.job }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - result-encoding: string - script: | - const { data: pull } = await github.rest.pulls.get({ - ...context.repo, - pull_number: process.env.number - }); - const ref = pull.head.sha; - - const { data: checks } = await github.rest.checks.listForRef({ - ...context.repo, - ref - }); - - const check = checks.check_runs.filter(c => c.name === process.env.job); - - return check[0].id; - - - uses: actions/github-script@v7.0.1 - env: - check_id: ${{ steps.check-id.outputs.result }} - details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.checks.update({ - ...context.repo, - check_run_id: process.env.check_id, - status: 'in_progress', - details_url: process.env.details_url - }); - - # Check out merge commit - - name: Fork based /deploy checkout - uses: actions/checkout@v4.1.1 - with: - ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' - - # - - name: Context - uses: okteto/context@latest - with: - token: ${{ secrets.OKTETO_TOKEN }} - - - name: Deploy preview environment - uses: ikuradon/deploy-preview@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo - timeout: 15m - - # Update check run called "integration-fork" - - uses: actions/github-script@v7.0.1 - id: update-check-run - if: ${{ always() }} - env: - # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run - conclusion: ${{ job.status }} - check_id: ${{ steps.check-id.outputs.result }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { data: result } = await github.rest.checks.update({ - ...context.repo, - check_run_id: process.env.check_id, - status: 'completed', - conclusion: process.env.conclusion - }); - - return result; diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml deleted file mode 100644 index 8967eb2f94a4..000000000000 --- a/.github/workflows/pr-preview-destroy.yml +++ /dev/null @@ -1,54 +0,0 @@ -# file: .github/workflows/preview-closed.yaml -on: - pull_request: - types: - - closed - -name: Destroy preview environment - -jobs: - destroy-preview-environment: - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v7.0.1 - id: check-conclusion - env: - number: ${{ github.event.number }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - result-encoding: string - script: | - const { data: pull } = await github.rest.pulls.get({ - ...context.repo, - pull_number: process.env.number - }); - const ref = pull.head.sha; - - const { data: checks } = await github.rest.checks.listForRef({ - ...context.repo, - ref - }); - - const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment'); - - if (check.length === 0) { - return; - } - - const { data: result } = await github.rest.checks.get({ - ...context.repo, - check_run_id: check[0].id, - }); - - return result.conclusion; - - name: Context - if: steps.check-conclusion.outputs.result == 'success' - uses: okteto/context@latest - with: - token: ${{ secrets.OKTETO_TOKEN }} - - - name: Destroy preview environment - if: steps.check-conclusion.outputs.result == 'success' - uses: okteto/destroy-preview@latest - with: - name: pr-${{ github.event.number }}-syuilo diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index be294201f037..9e5a79faac43 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -26,12 +26,12 @@ jobs: NODE_OPTIONS: "--max_old_space_size=7168" steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 if: github.event_name != 'pull_request_target' with: fetch-depth: 0 submodules: true - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 if: github.event_name == 'pull_request_target' with: fetch-depth: 0 @@ -46,7 +46,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 - name: Use Node.js 20.x - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 99a9f99b7599..2b8092cf4540 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -45,7 +45,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: submodules: true - name: Install pnpm @@ -66,7 +66,7 @@ jobs: fi done - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -108,13 +108,13 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: submodules: true - name: Install pnpm uses: pnpm/action-setup@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml index 74cdff80f11b..e7b5d7b09804 100644 --- a/.github/workflows/test-federation.yml +++ b/.github/workflows/test-federation.yml @@ -47,7 +47,7 @@ jobs: fi done - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 6c09896d0609..e489ebf07c31 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -36,13 +36,13 @@ jobs: node-version: [22.11.0] steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: submodules: true - name: Install pnpm uses: pnpm/action-setup@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -86,7 +86,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: submodules: true # https://github.com/cypress-io/cypress-docker-images/issues/150 @@ -98,7 +98,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index d88dc18bd804..05f757acc1c6 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -31,12 +31,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.2 - run: corepack enable - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 5269358e382a..56e42213ffd9 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -21,13 +21,13 @@ jobs: node-version: [22.11.0] steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: submodules: true - name: Install pnpm uses: pnpm/action-setup@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml index 65afcd4cd004..a8b2402988f4 100644 --- a/.github/workflows/validate-api-json.yml +++ b/.github/workflows/validate-api-json.yml @@ -25,13 +25,13 @@ jobs: node-version: [22.11.0] steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.2.2 with: submodules: true - name: Install pnpm uses: pnpm/action-setup@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.1.0 + uses: actions/setup-node@v4.2.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/CHANGELOG.md b/CHANGELOG.md index 5acfb717277f..53cf1b0cca4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +## 2025.2.1 + +### General +- Feat: アクセストークン発行時に通知するように +- Feat: 実験的なGoogleAnalyticsサポートを追加 +- 依存関係の更新 + +### Client +- Feat: 投稿フォームで画像をプレビュー可能に +- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように +- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992` +- Enhance: クライアントエラー画面の多言語対応 +- Enhance: 開発者モードでメニューからファイルIDをコピー出来るように `#15441' +- Enhance: ノートに埋め込まれたメディアのコンテキストメニューから管理者用のファイル管理画面を開けるように ( #15440 ) +- Enhance: リアクションする際に確認ダイアログを表示できるように +- Enhance: コントロールパネルのユーザ検索で入力された情報をページ遷移で損なわないように `#15437` +- Enhance: CWの注釈で入力済みの文字数を表示 +- Enhance: ノート検索ページのデザイン調整 + (Cherry-picked from https://github.com/taiyme/misskey/pull/273) +- Fix: ノートページで、クリップ一覧が表示されないことがある問題を修正 +- Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529` +- Fix: 埋め込みプレイヤーから外部ページに移動できない問題を修正 +- Fix: Play の再読込時に UI が以前の状態を引き継いでしまう問題を修正 `#14378` +- Fix: カスタム絵文字管理画面(beta)にてisSensitive/localOnlyの絞り込みが上手くいかない問題の修正 ( #15445 ) +- Fix: ユーザのサジェスト中に@を入力してもサジェスト結果が消えないように `#14385` +- Fix: CWの注釈が100文字を超えている場合、ノート投稿ボタンを非アクティブに +- Fix: テーマ選択で現在のテーマが初期表示されていない問題を修正 +- 翻訳の更新 + +### Server +- Enhance: 成り済まし対策として、ActivityPub照会された時にリモートのリダイレクトを拒否できるように (config.disallowExternalApRedirect) +- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように +- Fix: オブジェクトストレージの設定でPrefixを設定していなかった場合nullまたは空文字になる問題を修正 +- Fix: HTTPプロキシとその除外設定を行った状態でカスタム絵文字の一括インポートをしたとき、除外設定が効かないのを修正( #8766 ) +- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正 + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886) +- Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように +- Fix: `update-meta`でobjectStoragePrefixにS3_SAFEかつURL-safeでない文字列を使えないように +- Fix: クリップの説明欄を更新する際に空にできない問題を修正 +- Fix: フォロワーではないユーザーにリノートもしくは返信された場合にノートのDeleteアクティビティが送られていない問題を修正 + ## 2025.2.0 ### General diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 91c90ce75a07..1a3eeaead312 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1584,3 +1584,7 @@ _offlineScreen: _remoteLookupErrors: _noSuchObject: title: "غير موجود" +_search: + searchScopeAll: "الكل" + searchScopeLocal: "المحلي" + searchScopeUser: "مستخدم محدد" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 709874ac20de..d9a58ce8242c 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1348,3 +1348,6 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "পাওয়া যায়নি" +_search: + searchScopeAll: "সবগুলো" + searchScopeLocal: "স্থানীয়" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 7b029c6f416b..a42ebb0a5ae8 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -66,7 +66,7 @@ copyFolderId: "Copiar ID de la carpeta" copyProfileUrl: "Copiar adreça URL del perfil" searchUser: "Cercar un usuari" searchThisUsersNotes: "Cercar les publicacions de l'usuari" -reply: "Respon" +reply: "Respostes" loadMore: "Carregar més" showMore: "Veure més" showLess: "Mostrar menys" @@ -111,7 +111,7 @@ followRequests: "Peticions de seguiment" unfollow: "Deixar de seguir" followRequestPending: "Sol·licituds de seguiment pendents" enterEmoji: "Introduir un emoji" -renote: "Impulsar " +renote: "Impulsos" unrenote: "Anul·la l'impuls" renoted: "S'ha impulsat" renotedToX: "Impulsat per {name}." @@ -646,7 +646,7 @@ disablePlayer: "Tanca el reproductor de vídeo" expandTweet: "Expandir post" themeEditor: "Editor de temes" description: "Descripció" -describeFile: "Afegir subtitulació" +describeFile: "Afegeix una descripció " enterFileDescription: "Escriu un peu de foto" author: "Autor" leaveConfirm: "Hi ha canvis sense guardar. Els vols descartar?" @@ -1189,8 +1189,8 @@ currentAnnouncements: "Informes actuals" pastAnnouncements: "Informes passats" youHaveUnreadAnnouncements: "Tens informes per llegir." useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey." -replies: "Respon" -renotes: "Impulsar " +replies: "Respostes" +renotes: "Impulsos" loadReplies: "Mostrar les respostes" loadConversation: "Mostrar la conversació " pinnedList: "Llista fixada" @@ -1309,6 +1309,8 @@ availableRoles: "Roles disponibles " acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perills." federationSpecified: "Aquest servidor treballa amb una federació de llistes blanques. No pot interactuar amb altres servidors que no siguin els especificats per l'administrador." federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors." +confirmOnReact: "Confirmar en reaccionar" +reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?" _accountSettings: requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut" requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació." @@ -2440,6 +2442,8 @@ _notification: flushNotification: "Netejar notificacions" exportOfXCompleted: "Completada l'exportació de {x}" login: "Algú ha iniciat sessió " + createToken: "Token d'accés generat" + createTokenDescription: "Si no saps què és, esborra el token des de {text}." _types: all: "Tots" note: "Notes noves" @@ -2822,8 +2826,6 @@ _remoteLookupErrors: _responseInvalid: title: "La resposta no és correcta " description: "Hem pogut comunicar-nos amb aquest servidor, però les dades rebudes no són correctes." - _responseInvalidIdHostNotMatch: - description: "El domini de l'adreça introduïda no és el mateix que el domini de l'adreça final obtinguda. Si estàs consultant continguts remots mitjançant servidors tercers, torna a fer la consulta fent servir l'adreça que es pot obtenir en el servidor origen." _noSuchObject: title: "No s'ha trobat" description: "No es pot trobar el recurs sol·licitat, si us plau comprova l'adreça una altra vegada." @@ -2840,3 +2842,23 @@ _captcha: _unknown: title: "Error CAPTCHA" text: "S'ha produït un error inesperat." +_bootErrors: + title: "Hi ha hagut en error en carregar" + serverError: "Si el problema persisteix després d'esperar una mica i recarregar, posa't en contacte amb l'administrador del servidor amb el següent codi d'error." + solution: "Per intentar resoldre el problema pots fer el següent." + solution1: "Actualitza el navegador i el sistema operatiu a l'última versió " + solution2: "Desactiva els adblockers" + solution3: "Esborra la memòria cau del navegador" + solution4: "(Navegador Tor) configura dom.webaudio.enabled a true" + otherOption: "Altres opcions" + otherOption1: "Esborrar la configuració i la memòria cau del client" + otherOption2: "Iniciar client senzill" + otherOption3: "Iniciar l'eina de reparació " +_search: + searchScopeAll: "Tot" + searchScopeLocal: "Local" + searchScopeServer: "Instància " + searchScopeUser: "Especificar usuari" + pleaseEnterServerHost: "Introdueix l'adreça de la instància " + pleaseSelectUser: "Selecciona un usuari" + serverHostPlaceholder: "Ex: misskey.example.com" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index afa3047c1d33..fea8902ab8e5 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -2024,3 +2024,7 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "Nenalezeno" +_search: + searchScopeAll: "Vše" + searchScopeLocal: "Místní" + searchScopeUser: "Upřesnit uživatele" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 11fe6d3ff565..30f857459454 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1383,6 +1383,9 @@ _initialTutorial: title: "Was sind Notizen?" description: "Beiträge auf Misskey heißen \"Notizen\". Notizen werden chronologisch in der Chronik angeordnet und in Echtzeit aktualisiert." reply: "Klicke auf diesen Button, um auf eine Nachricht zu antworten. Es ist auch möglich, auf Antworten zu antworten und die Unterhaltung wie einen Thread fortzusetzen." + renote: "Du kannst diese Notiz in deiner eigenen Chronik teilen. Du kannst sie auch mit deinen Kommentaren zitieren." + reaction: "Du kannst der Notiz Reaktionen hinzufügen. Weitere Einzelheiten werden auf der nächsten Seite erläutert." + menu: "Du kannst Details zu Notizen anzeigen, Links kopieren und verschiedene andere Aktionen durchführen." _reaction: title: "Was sind Reaktionen?" description: "Auf Notizen kann mit verschiedenen Emojis reagiert werden. Reaktionen ermöglichen es dir, Nuancen auszudrücken, die mit einem einfachen „Gefällt mir“ vielleicht nicht ausgedrückt werden können." @@ -1401,13 +1404,21 @@ _initialTutorial: _visibility: description: "Du kannst einschränken, wer deine Notiz sehen kann." public: "Deine Notiz wird für alle Nutzer sichtbar sein." + direct: "Die Notiz wird nur für den angegebenen Benutzer veröffentlicht und der Empfänger wird benachrichtigt. Kann anstelle von Direktnachrichten verwendet werden." doNotSendConfidencialOnDirect1: "Sei vorsichtig, wenn du sensible Informationen verschickst!" + doNotSendConfidencialOnDirect2: "Die Administratoren des Servers können den Inhalt der Notiz sehen. Sei vorsichtig mit sensiblen Informationen, wenn du Direktnachrichten an Benutzer auf nicht vertrauenswürdigen Servern sendest." + localOnly: "Wenn du eine Notiz mit dieser Einstellung veröffentlichst, wird sie nicht an andere Server weitergeleitet. Benutzer auf anderen Servern können diese Notizen nicht direkt sehen, unabhängig von den obigen Anzeigeeinstellungen." _cw: title: "Inhaltswarnung" + description: "Anstelle des Textes wird das angezeigt, was du im Abschnitt „Anmerkungen“ angibst. Drücke auf „Inhalt anzeigen“, um den vollständigen Text zu sehen." _exampleNote: + cw: "Das wird dich bestimmt hungrig machen!" note: "Ich hatte gerade einen Donut mit Schokoladenüberzug 🍩😋" _howToMakeAttachmentsSensitive: + title: "Wie markiert man Anhänge als sensibel?" tryThisFile: "Versuche, das angehängte Bild als sensibel zu markieren!" + _exampleNote: + note: "Ups, ich habe es vergeigt, den Natto-Deckel zu öffnen..." method: "Um einen Anhang als sensibel zu kennzeichnen, klicke auf das Vorschaubild der Datei, um das Menü zu öffnen, und klicke auf „Als sensibel markieren“." sensitiveSucceeded: "Wenn du Dateien anhängst, stelle bitte die Sensibilität entsprechend der Serverrichtlinien ein." doItToContinue: "Markiere die angehängte Datei als sensibel, um fortzufahren." @@ -1431,6 +1442,7 @@ _serverSettings: fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden." fanoutTimelineDbFallback: "Auf die Datenbank zurückfallen" fanoutTimelineDbFallbackDescription: "Ist diese Option aktiviert, wird die Chronik auf zusätzliche Abfragen in der Datenbank zurückgreifen, wenn sich die Chronik nicht im Cache befindet. Eine Deaktivierung führt zu geringerer Serverlast, aber schränkt den Zeitraum der abrufbaren Chronik ein. " + reactionsBufferingDescription: "Wenn diese Option aktiviert ist, kann sie die Leistung beim Erstellen von Reaktionen erheblich verbessern und die Belastung der Datenbank verringern. Allerdings steigt die Speichernutzung von Redis." openRegistrationWarning: "Das Aktivieren von Registrierungen ist riskant. Es wird empfohlen, sie nur dann zu aktivieren, wenn der Server ständig überwacht wird und im Falle eines Problems sofort reagiert werden kann." thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern." _accountMigration: @@ -1835,6 +1847,7 @@ _plugin: installWarn: "Installiere bitte nur vertrauenswürdige Plugins." manage: "Plugins verwalten" viewSource: "Quelltext anzeigen" + viewLog: "Protokoll anzeigen" _preferencesBackups: list: "Erstellte Backups" saveNew: "Neu erstellen" @@ -1864,6 +1877,8 @@ _aboutMisskey: contributors: "Hauptmitwirkende" allContributors: "Alle Mitwirkenden" source: "Quellcode" + original: "Original" + thisIsModifiedVersion: "{name} verwendet eine modifizierte Version des ursprünglichen Misskey." translation: "Misskey übersetzen" donate: "An Misskey spenden" morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰" @@ -1989,6 +2004,8 @@ _soundSettings: driveFileTypeWarn: "Diese Datei wird nicht unterstützt" driveFileTypeWarnDescription: "Bitte wähle eine Audiodatei" driveFileDurationWarn: "Audio zu lang." + driveFileDurationWarnDescription: "Lange Töne kann die Verwendung von Misskey stören. Trotzdem fortfahren?" + driveFileError: "Audio konnte nicht geladen werden. Bitte ändere die Einstellung." _ago: future: "Zukunft" justNow: "Gerade eben" @@ -2000,6 +2017,10 @@ _ago: monthsAgo: "vor {n} Monat(en)" yearsAgo: "vor {n} Jahr(en)" invalid: "Ungültig" +_timeIn: + seconds: "In {n}s" + minutes: "In {n} Min." + hours: "In {n} Std." _time: second: "Sekunde(n)" minute: "Minute(n)" @@ -2105,6 +2126,7 @@ _auth: permissionAsk: "Diese Anwendung fordert folgende Berechtigungen" pleaseGoBack: "Bitte kehre zur Anwendung zurück" callback: "Es wird zur Anwendung zurückgekehrt" + accepted: "Zugriff gewährt" denied: "Zugriff verweigert" pleaseLogin: "Bitte logge dich ein, um Apps zu authorisieren." _antennaSources: @@ -2215,6 +2237,7 @@ _profile: changeBanner: "Banner ändern" verifiedLinkDescription: "Gibst du hier eine URL ein, die einen Link zu deinem Profile enthält, wird neben diesem Feld ein Icon zur Besitzbestätigung angezeigt." avatarDecorationMax: "Du kannst bis zu {max} Dekorationen hinzufügen." + followedMessage: "Nachricht, wenn dir jemand folgt" followedMessageDescription: "Du kannst eine kurze Nachricht festlegen, die dem Empfänger angezeigt wird, wenn er dir folgt." _exportOrImport: allNotes: "Alle Notizen" @@ -2343,8 +2366,11 @@ _notification: sendTestNotification: "Testbenachrichtigung senden" notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus" reactedBySomeUsers: "{n} Benutzer haben eine Reaktion geschickt" + likedBySomeUsers: "{n} Benutzer mochten deine Notiz" renotedBySomeUsers: "Renote von {n} Benutzern" followedBySomeUsers: "Von {n} Benutzern gefolgt" + flushNotification: "Benachrichtigungen löschen" + exportOfXCompleted: "Der Export von {x} ist abgeschlossen" login: "Neue Anmeldung erfolgt" _types: all: "Alle" @@ -2360,7 +2386,9 @@ _notification: followRequestAccepted: "Akzeptierte Follow-Anfragen" roleAssigned: "Rolle zugewiesen" achievementEarned: "Errungenschaft freigeschaltet" - login: "Anmelden" + exportCompleted: "Der Export ist abgeschlossen" + login: "Anmeldung" + test: "Test-Benachrichtigungen" app: "Benachrichtigungen von Apps" _actions: followBack: "folgt dir nun auch" @@ -2370,6 +2398,7 @@ _deck: alwaysShowMainColumn: "Hauptspalte immer zeigen" columnAlign: "Spaltenausrichtung" addColumn: "Spalte hinzufügen" + newNoteNotificationSettings: "Benachrichtigungseinstellungen für neue Notizen" configureColumn: "Spalteneinstellungen" swapLeft: "Mit linker Spalte tauschen" swapRight: "Mit rechter Spalte tauschen" @@ -2408,6 +2437,7 @@ _drivecleaner: orderByCreatedAtAsc: "Aufsteigendes Erstelldatum" _webhookSettings: createWebhook: "Webhook erstellen" + modifyWebhook: "Webhook bearbeiten" name: "Name" secret: "Secret" trigger: "Auslöser" @@ -2420,12 +2450,22 @@ _webhookSettings: renote: "Wenn du ein Renote erhältst" reaction: "Wenn du eine Reaktion erhältst" mention: "Wenn du erwähnt wirst" + deleteConfirm: "Bist du sicher, dass du den Webhook löschen willst?" _abuseReport: _notificationRecipient: createRecipient: "Meldungsempfänger hinzufügen" + modifyRecipient: "Bearbeite einen Empfänger für Meldungen" + recipientType: "Art der Benachrichtigung" _recipientType: mail: "Email" + webhook: "Webhook" + _captions: + mail: "Die Benachrichtigung wird bei Eingang einer Meldung an die E-Mail-Adressen der Moderatoren gesendet" + webhook: "Sendet eine Benachrichtigung an den System Webhook, wenn eine Meldung eingegangen ist oder gelöst wurde" keywords: "Schlüsselwort" + notifiedUser: "Zu benachrichtigender Benutzer" + notifiedWebhook: "Zu verwendender Webhook" + deleteConfirm: "Bist du sicher, dass du den Empfänger der Benachrichtigung entfernen möchtest?" _moderationLogTypes: createRole: "Rolle erstellt" deleteRole: "Rolle gelöscht" @@ -2465,6 +2505,7 @@ _moderationLogTypes: createSystemWebhook: "System-Webhook erstellt" updateSystemWebhook: "System-Webhook aktualisiert" deleteSystemWebhook: "System-Webhook gelöscht" + deleteAccount: "Benutzerkonto gelöscht" deletePage: "Seite gelöscht" deleteGalleryPost: "Galeriebeitrag gelöscht" _fileViewer: @@ -2573,3 +2614,7 @@ _remoteLookupErrors: _noSuchObject: title: "Nicht gefunden" description: "Die angeforderte Ressource konnte nicht gefunden werden, bitte überprüfe die URI erneut." +_search: + searchScopeAll: "Alle" + searchScopeLocal: "Lokal" + searchScopeUser: "Spezifischer Benutzer" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 4657842ca5bf..272ec07d8924 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -397,3 +397,5 @@ _moderationLogTypes: suspend: "Αποβολή" _reversi: total: "Σύνολο" +_search: + searchScopeLocal: "Τοπικό" diff --git a/locales/en-US.yml b/locales/en-US.yml index 6ff7e5fb7ce5..04ddd2966a55 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -132,7 +132,7 @@ reaction: "Reactions" reactions: "Reactions" emojiPicker: "Emoji picker" pinnedEmojisForReactionSettingDescription: "Set the emojis to be pinned and displayed when reacting." -pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker." +pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker" emojiPickerDisplay: "Emoji picker display" overwriteFromPinnedEmojisForReaction: "Override from reaction settings" overwriteFromPinnedEmojis: "Override from general settings" @@ -586,7 +586,7 @@ popout: "Pop-out" volume: "Volume" masterVolume: "Master volume" notUseSound: "Disable sound" -useSoundOnlyWhenActive: "Output sounds only if Misskey is active." +useSoundOnlyWhenActive: "Output sounds only if Misskey is active" details: "Details" renoteDetails: "Renote details" chooseEmoji: "Select an emoji" @@ -1261,7 +1261,7 @@ copyReplayData: "Copy replay data" ranking: "Ranking" lastNDays: "Last {n} days" backToTitle: "Go back to title" -hemisphere: "Where are you located" +hemisphere: "Where you live" withSensitive: "Include notes with sensitive files" userSaysSomethingSensitive: "Post by {name} contains sensitive content" enableHorizontalSwipe: "Swipe to switch tabs" @@ -1309,6 +1309,8 @@ availableRoles: "Available roles" acknowledgeNotesAndEnable: "Turn on after understanding the precautions." federationSpecified: "This server is operated in a whitelist federation. Interacting with servers other than those designated by the administrator is not allowed." federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers." +confirmOnReact: "Confirm when reacting" +reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?" _accountSettings: requireSigninToViewContents: "Require sign-in to view contents" requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information." @@ -2440,6 +2442,8 @@ _notification: flushNotification: "Clear notifications" exportOfXCompleted: "Export of {x} has been completed" login: "Someone logged in" + createToken: "An access token has been created" + createTokenDescription: "If you have no idea, delete the access token through \"{text}\"." _types: all: "All" note: "New notes" @@ -2822,8 +2826,6 @@ _remoteLookupErrors: _responseInvalid: title: "Response is invalid" description: "It could communicate with this server, but the data obtained was incorrect." - _responseInvalidIdHostNotMatch: - description: "The domain of the entered URI differs from the domain of the final obtained URI. If you are looking up remote content through a third-party server, please look up again using a URI that can be obtained from the origin server." _noSuchObject: title: "Not found" description: "The requested resource was not found, please recheck the URI." @@ -2840,3 +2842,19 @@ _captcha: _unknown: title: "CAPTCHA error" text: "An unexpected error occurred." +_bootErrors: + title: "Failed to load" + serverError: "If the problem persists after waiting a moment and reloading, please contact the server administrator with the following Error ID." + solution: "The following may solve the problem." + solution1: "Update your browser and OS to the latest version" + solution2: "Disable ad blocker" + solution3: "Clear the browser cache" + solution4: "Set the dom.webaudio.enabled to true for Tor Browser" + otherOption: "Other options" + otherOption1: "Delete client settings and cache" + otherOption2: "Start the simple client" + otherOption3: "Launch the repair tool" +_search: + searchScopeAll: "All" + searchScopeLocal: "Local" + searchScopeUser: "Specific user" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 0b1411d84b2f..070bc397cb12 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -605,6 +605,7 @@ descendingOrder: "Descendente" scratchpad: "Scratch pad" scratchpadDescription: "Scratchpad proporciona un entorno experimental para AiScript. Puede escribir, ejecutar y verificar los resultados que interactúan con Misskey." uiInspector: "Inspector de UI" +uiInspectorDescription: "Puedes visualizar una lista de elementos UI presentes en la memoria. Los componentes de la interfaz de usuario son generados por las funciones UI:C:" output: "Salida" script: "Script" disablePagesScript: "Deshabilitar AiScript en Páginas" @@ -693,6 +694,7 @@ regexpError: "Error de la expresión regular" regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line} de las palabras muteadas {tab}" instanceMute: "Instancias silenciadas" userSaysSomething: "{name} dijo algo" +userSaysSomethingAbout: "{name} dijo algo sobre {word}" makeActive: "Activar" display: "Apariencia" copy: "Copiar" @@ -861,6 +863,7 @@ administration: "Administrar" accounts: "Cuentas" switch: "Cambiar" noMaintainerInformationWarning: "No se ha establecido la información del administrador" +noInquiryUrlWarning: "No se ha guardado la URL de consulta." noBotProtectionWarning: "La protección contra los bots no está configurada" configure: "Configurar" postToGallery: "Crear una nueva publicación en la galería" @@ -925,6 +928,7 @@ followersVisibility: "Visibilidad de seguidores" continueThread: "Ver la continuación del hilo" deleteAccountConfirm: "La cuenta será borrada. ¿Está seguro?" incorrectPassword: "La contraseña es incorrecta" +incorrectTotp: "La contraseña de un solo uso es incorrecta o ha caducado." voteConfirm: "¿Confirma su voto a {choice}?" hide: "Ocultar" useDrawerReactionPickerForMobile: "Mostrar panel de reacciones en móviles" @@ -1053,6 +1057,7 @@ thisPostMayBeAnnoyingHome: "Publicar en línea de tiempo 'Inicio'" thisPostMayBeAnnoyingCancel: "detener" thisPostMayBeAnnoyingIgnore: "Publicar de todos modos" collapseRenotes: "Colapsar renotas que ya hayas visto" +collapseRenotesDescription: "Contrae notas a las que ya has reaccionado o renotado " internalServerError: "Error interno del servidor" internalServerErrorDescription: "El servidor tuvo un error inesperado." copyErrorInfo: "Copiar detalles del error" @@ -1091,6 +1096,7 @@ retryAllQueuesConfirmTitle: "Desea ¿reintentar inmediatamente todas las colas?" retryAllQueuesConfirmText: "La carga del servidor está incrementándose temporalmente " enableChartsForRemoteUser: "Generar gráficas de usuarios remotos." enableChartsForFederatedInstances: "Generar gráficos de servidores remotos" +enableStatsForFederatedInstances: "Activar las estadísticas de las instancias remotas federadas" showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas" reactionsDisplaySize: "Tamaño de las reacciones" limitWidthOfReaction: "Limitar ancho de las reacciones" @@ -1139,6 +1145,7 @@ preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los text options: "Opción" specifyUser: "Especificar usuario" lookupConfirm: "¿Quiere informarse?" +openTagPageConfirm: "¿Quieres abrir la página de etiquetas?" specifyHost: "Especificar Host" failedToPreviewUrl: "No se pudo generar la vista previa" update: "Actualizar" @@ -1267,13 +1274,29 @@ useBackupCode: "Usar códigos de respaldo" launchApp: "Ejecutar la app" useNativeUIForVideoAudioPlayer: "Usar la interfaz del navegador cuando se reproduce audio y vídeo" keepOriginalFilename: "Mantener el nombre original del archivo" +keepOriginalFilenameDescription: "Si desactivas esta opción, los nombres de los archivos serán remplazados por una cadena de caracteres aleatoria cuando subas los archivos." noDescription: "No hay descripción" alwaysConfirmFollow: "Confirmar siempre cuando se sigue a alguien" inquiry: "Contacto" tryAgain: "Por favor , inténtalo de nuevo" +confirmWhenRevealingSensitiveMedia: "Confirmación cuando se revele contenido sensible" +sensitiveMediaRevealConfirm: "Esto puede contener contenido sensible. ¿Estás seguro/a de querer mostrarlo?" +createdLists: "Listas creadas" +createdAntennas: "Antenas creadas" +fromX: "De {x}" +genEmbedCode: "Obtener el código para incrustar" +noteOfThisUser: "Notas de este usuario" +clipNoteLimitExceeded: "No se pueden añadir más notas a este clip." performance: "Rendimiento" +modified: "Modificado" +discard: "Descartar" +thereAreNChanges: "Hay {n} cambio(s)" +signinWithPasskey: "Iniciar sesión con clave de acceso" unknownWebAuthnKey: "Esto no se ha registrado llave maestra." +passkeyVerificationFailed: "La verificación de la clave de acceso ha fallado." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificación de la clave de acceso ha sido satisfactoria pero se ha deshabilitado el inicio de sesión sin contraseña." messageToFollower: "Mensaje a seguidores" +target: "Para" federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador." federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores" _accountSettings: @@ -2560,6 +2583,13 @@ _mediaControls: pip: "Picture in Picture" playbackRate: "Velocidad de reproducción" loop: "Reproducción en bucle" +_followRequest: + recieved: "Petición de seguimiento recibida" + sent: "Petición de seguimiento enviada" _remoteLookupErrors: _noSuchObject: title: "No se encuentra" +_search: + searchScopeAll: "Todo" + searchScopeLocal: "Local" + searchScopeUser: "Especificar usuario" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index ccfd462a76cc..09a4d4bc8420 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -2364,3 +2364,7 @@ _embedCodeGen: _remoteLookupErrors: _noSuchObject: title: "Non trouvé" +_search: + searchScopeAll: "Tous" + searchScopeLocal: "Local" + searchScopeUser: "Spécifier l'utilisateur·rice" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 7be56b14941b..75e420aac778 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -2610,3 +2610,7 @@ _mediaControls: _remoteLookupErrors: _noSuchObject: title: "Tidak dapat ditemukan" +_search: + searchScopeAll: "Semua" + searchScopeLocal: "Lokal" + searchScopeUser: "Pengguna spesifik" diff --git a/locales/index.d.ts b/locales/index.d.ts index 418e5436e3a7..f90ca118f660 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5258,6 +5258,14 @@ export interface Locale extends ILocale { * このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。 */ "federationDisabled": string; + /** + * リアクションする際に確認する + */ + "confirmOnReact": string; + /** + * " {emoji} " をリアクションしますか? + */ + "reactAreYouSure": ParameterizedString<"emoji">; "_accountSettings": { /** * コンテンツの表示にログインを必須にする @@ -9496,6 +9504,14 @@ export interface Locale extends ILocale { * ログインがありました */ "login": string; + /** + * アクセストークンが作成されました + */ + "createToken": string; + /** + * 心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。 + */ + "createTokenDescription": ParameterizedString<"text">; "_types": { /** * すべて @@ -10904,13 +10920,7 @@ export interface Locale extends ILocale { */ "title": string; /** - * このサーバーと通信することはできましたが、得られたデータが不正なものでした。 - */ - "description": string; - }; - "_responseInvalidIdHostNotMatch": { - /** - * 入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。 + * このサーバーと通信することはできましたが、得られたデータが不正なものでした。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。 */ "description": string; }; @@ -10968,6 +10978,82 @@ export interface Locale extends ILocale { }; }; }; + "_bootErrors": { + /** + * 読み込みに失敗しました + */ + "title": string; + /** + * 少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。 + */ + "serverError": string; + /** + * 以下を行うと解決する可能性があります。 + */ + "solution": string; + /** + * ブラウザおよびOSを最新バージョンに更新する + */ + "solution1": string; + /** + * アドブロッカーを無効にする + */ + "solution2": string; + /** + * ブラウザのキャッシュをクリアする + */ + "solution3": string; + /** + * (Tor Browser) dom.webaudio.enabledをtrueに設定する + */ + "solution4": string; + /** + * その他のオプション + */ + "otherOption": string; + /** + * クライアント設定とキャッシュを削除 + */ + "otherOption1": string; + /** + * 簡易クライアントを起動 + */ + "otherOption2": string; + /** + * 修復ツールを起動 + */ + "otherOption3": string; + }; + "_search": { + /** + * 全て + */ + "searchScopeAll": string; + /** + * ローカル + */ + "searchScopeLocal": string; + /** + * サーバー指定 + */ + "searchScopeServer": string; + /** + * ユーザー指定 + */ + "searchScopeUser": string; + /** + * サーバーのホストを入力してください + */ + "pleaseEnterServerHost": string; + /** + * ユーザーを選択してください + */ + "pleaseSelectUser": string; + /** + * 例: misskey.example.com + */ + "serverHostPlaceholder": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/it-IT.yml b/locales/it-IT.yml index c233e3ab87d7..eaf9e81b0ecc 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1309,6 +1309,8 @@ availableRoles: "Ruoli disponibili" acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento." federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione." federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server." +confirmOnReact: "Confermare le reazioni" +reactAreYouSure: "Vuoi davvero reagire con {emoji} ?" _accountSettings: requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione" requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler." @@ -2440,6 +2442,8 @@ _notification: flushNotification: "Azzera le notifiche" exportOfXCompleted: "Abbiamo completato l'esportazione di {x}" login: "Autenticazione avvenuta" + createToken: "È stato creato un token di accesso" + createTokenDescription: "In caso contrario, eliminare il token di accesso tramite ({text})." _types: all: "Tutto" note: "Nuove Note" @@ -2822,8 +2826,6 @@ _remoteLookupErrors: _responseInvalid: title: "Risposta non valida" description: "La comunicazione col server è andata a buon fine, ma abbiamo ricevuto dati non validi." - _responseInvalidIdHostNotMatch: - description: "L'indirizzo immesso non coincide con la URL finale. Interrogando i server per un contenuto remoto, assicurarsi di utilizzare la URL finale e non quella di un server intermedio." _noSuchObject: title: "Non trovato" description: "La risorsa richiesta non è stata trovata. Verificare nuovamente la URL." @@ -2840,3 +2842,19 @@ _captcha: _unknown: title: "Errore CAPTCHA" text: "Si è verificato un errore imprevisto." +_bootErrors: + title: "Caricamento non riuscito" + serverError: "Dopo una breve attesa, e dopo aver ricaricato la pagina, se il problema persiste, contatta l'amministrazione comunicando il seguente ID di errore." + solution: "Di seguito, alcune probabili soluzioni al problema." + solution1: "Aggiornare browser e il sistema operativo all'ultima versione" + solution2: "Disattivare gli adblocker" + solution3: "Cancellare la cache del browser" + solution4: "(Per chi utilizza il Browser Tor) Impostare dom.webaudio.enabled = vero" + otherOption: "Altre opzioni" + otherOption1: "Nelle impostazioni, cancellare le impostazioni del client e svuotare la cache" + otherOption2: "Avviare il client predefinito" + otherOption3: "Avviare lo strumento di riparazione" +_search: + searchScopeAll: "Tutte" + searchScopeLocal: "Locale" + searchScopeUser: "Profilo specifico" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3237083d5ab4..319abb4a296a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1310,6 +1310,8 @@ availableRoles: "利用可能なロール" acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。" federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" +confirmOnReact: "リアクションする際に確認する" +reactAreYouSure: "\" {emoji} \" をリアクションしますか?" _accountSettings: requireSigninToViewContents: "コンテンツの表示にログインを必須にする" @@ -2506,6 +2508,8 @@ _notification: flushNotification: "通知の履歴をリセットする" exportOfXCompleted: "{x}のエクスポートが完了しました" login: "ログインがありました" + createToken: "アクセストークンが作成されました" + createTokenDescription: "心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。" _types: all: "すべて" @@ -2913,9 +2917,7 @@ _remoteLookupErrors: description: "このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。" _responseInvalid: title: "レスポンスが不正です" - description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。" - _responseInvalidIdHostNotMatch: - description: "入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。" + description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。" _noSuchObject: title: "見つかりません" description: "要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。" @@ -2933,3 +2935,25 @@ _captcha: _unknown: title: "CAPTCHAエラー" text: "想定外のエラーが発生しました。" + +_bootErrors: + title: "読み込みに失敗しました" + serverError: "少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。" + solution: "以下を行うと解決する可能性があります。" + solution1: "ブラウザおよびOSを最新バージョンに更新する" + solution2: "アドブロッカーを無効にする" + solution3: "ブラウザのキャッシュをクリアする" + solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する" + otherOption: "その他のオプション" + otherOption1: "クライアント設定とキャッシュを削除" + otherOption2: "簡易クライアントを起動" + otherOption3: "修復ツールを起動" + +_search: + searchScopeAll: "全て" + searchScopeLocal: "ローカル" + searchScopeServer: "サーバー指定" + searchScopeUser: "ユーザー指定" + pleaseEnterServerHost: "サーバーのホストを入力してください" + pleaseSelectUser: "ユーザーを選択してください" + serverHostPlaceholder: "例: misskey.example.com" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 66560f524b7f..f00eb6a6e3a4 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -5,6 +5,7 @@ introMisskey: "ようお越し!Misskeyは、オープンソースの分散型 poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォームMisskeyのサーバーのひとつなんやで。" monthAndDay: "{month}月 {day}日" search: "探す" +reset: "リセット" notifications: "通知" username: "ユーザー名" password: "パスワード" @@ -48,6 +49,7 @@ pin: "ピン留めしとく" unpin: "ピン留めやめる" copyContent: "内容をコピー" copyLink: "リンクをコピー" +copyRemoteLink: "リモートのリンクをコピーするで?" copyLinkRenote: "リノートのリンクをコピーするで?" delete: "ほかす" deleteAndEdit: "ほかして直す" @@ -684,11 +686,15 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecureInfo: "STARTTLS使っとる時はオフにしてや。" testEmail: "配信テスト" wordMute: "ワードミュート" +wordMuteDescription: "指定した語句が入ってるノートを最小化するで。最小化されたノートをクリックしたら、表示できるようになるで。" hardWordMute: "ハードワードミュート" +showMutedWord: "ミュートされたワードを表示するで" +hardWordMuteDescription: "指定した語句が入ってるノートを隠すで。ワードミュートとちゃうて、ノートは完全に表示されんようになるで。" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:" instanceMute: "サーバーミュート" userSaysSomething: "{name}が何か言うとるわ" +userSaysSomethingAbout: "{name}が「{word}」についてなんか言うてたで" makeActive: "使うで" display: "表示" copy: "コピー" @@ -1301,6 +1307,10 @@ lockdown: "ロックダウン" pleaseSelectAccount: "アカウント選んでや" availableRoles: "使えるロール" acknowledgeNotesAndEnable: "注意事項をわかった上でオンにする。" +federationSpecified: "このサーバーはホワイトリスト連合で運用されてるで。管理者が指定したサーバー以外とはやり取りできひんで。" +federationDisabled: "このサーバーは連合が無効化されてるで。他のサーバーのユーザーとやり取りすることはできひんで。" +confirmOnReact: "ツッコむときに確認とる" +reactAreYouSure: "\" {emoji} \" でツッコむ?" _accountSettings: requireSigninToViewContents: "ログインしてもらってからコンテンツ見てもらう" requireSigninToViewContentsDescription1: "あなたが作成した全部のノートとかのコンテンツを見れるようにするのにログインがいるようにするで。クローラーにいろいろ収集されるんを防げるかもしれん。" @@ -2432,6 +2442,8 @@ _notification: flushNotification: "通知の履歴をリセットする" exportOfXCompleted: "{x}のエクスポートが終わったわ" login: "ログインしとったで" + createToken: "アクセストークンが作成されたで" + createTokenDescription: "心当たりないんやったら「{text}」でアクセストークンを削除してやって。" _types: all: "すべて" note: "あんたらの新規投稿" @@ -2718,6 +2730,66 @@ _contextMenu: app: "アプリ" appWithShift: "Shiftキーでアプリ" native: "ブラウザのUI" +_gridComponent: + _error: + requiredValue: "この値は必須項目やで" + columnTypeNotSupport: "正規表現によるバリデーションはtype:textのカラムだけサポートしてるで" + patternNotMatch: "この値は{pattern}のパターンに一致しいひんで" + notUnique: "この値は一意でなあかんで" +_roleSelectDialog: + notSelected: "選択されとらんで" +_customEmojisManager: + _gridCommon: + copySelectionRows: "選択行をコピーするで" + copySelectionRanges: "選択範囲をコピーするで" + deleteSelectionRows: "選択行を削除するで" + deleteSelectionRanges: "選択範囲の値をクリアするで" + searchSettings: "検索設定" + searchSettingCaption: "検索条件を詳しく設定するで。" + searchLimit: "表示件数" + sortOrder: "並び順" + registrationLogs: "登録ログ" + registrationLogsCaption: "絵文字更新・削除時のログが表示されるで。更新・削除操作をしたり、ページを遷移・リロードしたら消えるから気ぃつけてな。" + alertEmojisRegisterFailedDescription: "絵文字の更新・削除に失敗したで。詳細は登録ログを確認してな。" + _logs: + showSuccessLogSwitch: "成功ログを表示するで" + failureLogNothing: "失敗ログはあらへん。" + logNothing: "失敗ログはあらへん。" + _remote: + selectionRowDetail: "選択行の詳細やで" + importSelectionRows: "選択行をインポートするで" + importSelectionRangesRows: "選択範囲の行をインポートするで" + importEmojisButton: "チェックされた絵文字をインポートするで" + confirmImportEmojisTitle: "絵文字のインポートするで" + confirmImportEmojisDescription: "リモートから受信した{count}個の絵文字をインポートするで。絵文字のライセンスには十分気ぃつけてな。実行してもええか?" + _local: + tabTitleList: "登録済み絵文字一覧" + tabTitleRegister: "絵文字の登録" + _list: + emojisNothing: "登録された絵文字はないで。" + markAsDeleteTargetRows: "選択行を削除対象にするで" + markAsDeleteTargetRanges: "選択範囲の行を削除対象にするで" + alertUpdateEmojisNothingDescription: "変更された絵文字はないで。" + alertDeleteEmojisNothingDescription: "削除対象の絵文字はないで。" + confirmMovePage: "ページを移動してもええんか?" + confirmChangeView: "表示を変更してもええんか?" + confirmUpdateEmojisDescription: "{count}個の絵文字を更新するで。実行してもええか?" + confirmDeleteEmojisDescription: "チェックがつけられた{count}個の絵文字を削除するで。ほんまにええか?" + confirmResetDescription: "今までやった変更が全部リセットされるで。" + confirmMovePageDesciption: "このページの絵文字に変更が加えられてるで。\n保存せずページを移動してまうと、このページで加えた変更が全てパーになるで。" + dialogSelectRoleTitle: "絵文字に設定されたロールで検索" + _register: + uploadSettingTitle: "アップロード設定" + uploadSettingDescription: "この画面で絵文字アップロードするときの動きを設定できるで。" + directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する" + directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。" + emojiInputAreaCaption: "どれかの方法で登録する絵文字を選択して。" + emojiInputAreaList1: "この枠に画像ファイルかディレクトリをドラッグ&ドロップ" + emojiInputAreaList2: "このリンクをクリックしてPCから選択する" + emojiInputAreaList3: "このリンクをクリックしてドライブから選択する" + confirmRegisterEmojisDescription: "リストに表示されてる絵文字を新たなカスタム絵文字として登録するで。ほんまにええか? (サーバーがしんどくなるから、一回で登録できる絵文字は{count}件までやで)" + confirmClearEmojisDescription: "編集内容をほかして、リストに表示されている絵文字をクリアするで。ほんまにええか?" + confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードするで。ほんまにええか?" _embedCodeGen: title: "埋め込みコードをカスタム" header: "ヘッダー出す" @@ -2754,8 +2826,35 @@ _remoteLookupErrors: _responseInvalid: title: "レスポンスがおかしいで" description: "このサーバーと通信することはできたけど、もらったデータがおかしかったで。" - _responseInvalidIdHostNotMatch: - description: "入力されたURIのドメインと最終的に得られたURIのドメインとが違うで。第三者のサーバーを介してリモートのコンテンツを照会してるんやったら、発信元のサーバーで取得できるURIを使って照会し直して。" _noSuchObject: title: "見つからへんね" description: "求められたリソースが見つからんかったで。URIをもっかい確かめてや。" +_captcha: + verify: "CAPTCHAしばいたって" + testSiteKeyMessage: "サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できるで。\n詳細は下記ページを確認してな。" + _error: + _requestFailed: + title: "CAPTCHAのリクエストに失敗してもうた" + text: "しばらく後で実行するか、設定をもっかい確認してや。" + _verificationFailed: + title: "CAPTCHAのリクエストに失敗してもうた" + text: "設定がほんまに合ってるかもっかい確認してや。" + _unknown: + title: "CAPTCHAエラー" + text: "思いもせんかったエラーが起きたわ。" +_bootErrors: + title: "読み込みに失敗したで" + serverError: "少し待ってからリロードしてもまだ問題が解決されんのやったら、以下のError IDを添えてサーバー管理者に連絡して。" + solution: "以下のことやったら解決するかもやで。" + solution1: "ブラウザとかOSを最新バージョンに更新する" + solution2: "アドブロッカーを無効にする" + solution3: "ブラウザのキャッシュをクリアする" + solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する" + otherOption: "ほかのオプション" + otherOption1: "クライアント設定とキャッシュをほかす" + otherOption2: "簡易クライアントを起動" + otherOption3: "修復ツールを起動" +_search: + searchScopeAll: "みんな" + searchScopeLocal: "ローカル" + searchScopeUser: "ユーザー指定" diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index 4b9650b636ea..ce83ef0e07f9 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -843,3 +843,6 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "몬 찾앗십니다" +_search: + searchScopeAll: "말캉" + searchScopeUser: "사용자 지정" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 36b818c11762..82b42ec2be38 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -2822,8 +2822,6 @@ _remoteLookupErrors: _responseInvalid: title: "유효하지 않은 반응입니다." description: "이 서버와 통신할 수 있지만, 데이터가 올바르지 않습니다." - _responseInvalidIdHostNotMatch: - description: "입력된 URI과 실제 URI가 다릅니다. 제 3자 서버를 통한 리모트 컨텐츠를 조회하는 경우, 원래 서버 측에서 받아올 수 있는 URI를 사용하여 조회하시길 바랍니다." _noSuchObject: title: "찾을 수 없습니다" description: "요구된 리소스를 찾을 수 없습니다. URI를 다시 한 번 확인해보세요." @@ -2840,3 +2838,10 @@ _captcha: _unknown: title: "CAPTCHA 에러" text: "알 수 없는 에러가 발생했습니다." +_bootErrors: + title: "로딩이 실패함" + solution4: "(Tor Browser) dom.webaudio.enabled를 true로 설정하세요" +_search: + searchScopeAll: "전체" + searchScopeLocal: "로컬" + searchScopeUser: "사용자 지정" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 2d55c289aa3e..06acda44be6c 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -477,3 +477,5 @@ _moderationLogTypes: _remoteLookupErrors: _noSuchObject: title: "ບໍ່ພົບ" +_search: + searchScopeAll: "ທັງໝົດ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 685094b4a58a..320b9d62baf3 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -540,3 +540,5 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "Niet gevonden" +_search: + searchScopeAll: "Alle" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 474e05ba67c3..c1b145cab47c 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -730,3 +730,5 @@ _moderationLogTypes: _remoteLookupErrors: _noSuchObject: title: "Ikke funnet" +_search: + searchScopeAll: "Alle" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 9bd585de862f..13f3b118fe94 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1583,3 +1583,6 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "Nie znaleziono" +_search: + searchScopeAll: "Wszystkie" + searchScopeLocal: "Lokalne" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index d691022d758f..8892e6574f00 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -2754,8 +2754,10 @@ _remoteLookupErrors: _responseInvalid: title: "Resposta inválida" description: "Foi possível comunicar com o servidor, porém os dados obtidos foram incorretos." - _responseInvalidIdHostNotMatch: - description: "O domínio do endereço inserido difere do domínio do endereço final. Se você estiver pesquisando por um servidor de terceiros, tente buscar novamente com um endereço que pode ser obtido através do servidor original." _noSuchObject: title: "Não encontrado" description: "O recurso solicitado não foi encontrado, confira o endereço." +_search: + searchScopeAll: "Todos" + searchScopeLocal: "Local" + searchScopeUser: "Usuário específico" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 07f4c98d969b..0d43e174a754 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -736,3 +736,5 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "Nu a fost găsit" +_search: + searchScopeAll: "Tot" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 7ed41a9c4757..5ba3bc8e8723 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -2147,3 +2147,7 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "Не найдено" +_search: + searchScopeAll: "Все" + searchScopeLocal: "Местная" + searchScopeUser: "Указанный пользователь" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 521d172671ff..91de8a33523f 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1449,3 +1449,6 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "Nenájdené" +_search: + searchScopeAll: "Všetko" + searchScopeLocal: "Lokálne" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 596160564520..91413e3bc749 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -707,3 +707,5 @@ _reversi: white: "Vit" _selfXssPrevention: warning: "VARNING" +_search: + searchScopeAll: "Allt" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index ec83ba888c02..b76c088b3fc4 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -2709,3 +2709,7 @@ _embedCodeGen: _remoteLookupErrors: _noSuchObject: title: "ไม่พบหน้าที่ต้องการ" +_search: + searchScopeAll: "ทั้งหมด" + searchScopeLocal: "ท้องถิ่น" + searchScopeUser: "ผู้ใช้เฉพาะ" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 2c63f15aa20d..e99aa64254c0 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -460,3 +460,5 @@ _deck: _moderationLogTypes: suspend: "askıya al" resetPassword: "Şifre sıfırlama" +_search: + searchScopeAll: "Tümü" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index a83ad8068381..4f7e6430e944 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1624,3 +1624,6 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "Не знайдено" +_search: + searchScopeAll: "Всі" + searchScopeLocal: "Локальна" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index 6015492b92f7..c70802e4174e 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -1094,3 +1094,6 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "Topilmadi" +_search: + searchScopeAll: "Barcha" + searchScopeLocal: "Mahalliy" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index e6a94181267a..628bd513fe09 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1930,3 +1930,7 @@ _reversi: _remoteLookupErrors: _noSuchObject: title: "Không tìm thấy" +_search: + searchScopeAll: "Tất cả" + searchScopeLocal: "Máy chủ này" + searchScopeUser: "Người dùng chỉ định" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index f4df425af487..1599db58544c 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1309,6 +1309,8 @@ availableRoles: "可用角色" acknowledgeNotesAndEnable: "理解注意事项后再开启。" federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。" federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。" +confirmOnReact: "发送回应前需要确认" +reactAreYouSure: "要用「{emoji}」进行回应吗?" _accountSettings: requireSigninToViewContents: "需要登录才能显示内容" requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。" @@ -2440,6 +2442,8 @@ _notification: flushNotification: "重置通知历史" exportOfXCompleted: "已完成 {x} 的导出" login: "有新的登录" + createToken: "访问令牌已创建" + createTokenDescription: "如果不明白其用途,请遵循「{text}」的指示删除访问令牌。" _types: all: "全部" note: "用户的新帖子" @@ -2777,8 +2781,8 @@ _customEmojisManager: _register: uploadSettingTitle: "上传设置" uploadSettingDescription: "可以在此页面设置上传表情符号时的行为。" - directoryToCategoryLabel: "目录名请输入「category」" - directoryToCategoryCaption: "拖放目录时,目录名请输入「category」" + directoryToCategoryLabel: "将目录名设为「category」" + directoryToCategoryCaption: "拖放目录时,将目录名设置为「category」" emojiInputAreaCaption: "请使用其中一种方法选择要注册的表情符号。" emojiInputAreaList1: "在此区域内拖放图像文件或者目录" emojiInputAreaList2: "单击此链接以从电脑中选择" @@ -2822,8 +2826,6 @@ _remoteLookupErrors: _responseInvalid: title: "响应无效" description: "成功与此服务器通信,但返回的数据无效。" - _responseInvalidIdHostNotMatch: - description: "输入 URI 的域名和最终取得的 URI 的域名不同。如果是通过第三方服务器获取远程内容,请使用可以从原始服务器获取内容的 URI 再试一次。" _noSuchObject: title: "未找到" description: "未找到请求的资源。请再次检查 URI。" @@ -2840,3 +2842,19 @@ _captcha: _unknown: title: "CAPTCHA 错误" text: "发生意外错误。" +_bootErrors: + title: "加载失败" + serverError: "请稍等片刻再重试。若问题仍无法解决,请将以下 Error ID 一起发送给管理员。" + solution: "以下方法或许可以解决问题:" + solution1: "将浏览器及操作系统更新到最新版本" + solution2: "禁用广告屏蔽插件" + solution3: "清除浏览器缓存" + solution4: "(Tor Browser)将 dom.webaudio.enabled 设定为 true" + otherOption: "其它选项" + otherOption1: "清除客户端设定与缓存" + otherOption2: "使用简易客户端" + otherOption3: "启动修复工具" +_search: + searchScopeAll: "全部" + searchScopeLocal: "本地" + searchScopeUser: "用户指定" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 466e3cc1d847..10079f2bee29 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -368,7 +368,7 @@ normal: "正常" instanceName: "伺服器名稱" instanceDescription: "伺服器介紹" maintainerName: "管理員名稱" -maintainerEmail: "管理員郵箱" +maintainerEmail: "管理員信箱" tosUrl: "服務條款 URL" thisYear: "本年" thisMonth: "本月" @@ -464,7 +464,7 @@ securityKey: "安全金鑰" lastUsed: "上次使用" lastUsedAt: "上次使用:{t}" unregister: "註銷" -passwordLessLogin: "設置無密碼登入" +passwordLessLogin: "無密碼登入" passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入" resetPassword: "重設密碼" newPasswordIs: "新密碼為「{password}」" @@ -521,7 +521,7 @@ menuStyle: "選單風格" style: "風格" drawer: "側邊欄" popup: "彈出式視窗" -showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的" +showNoteActionsOnlyHover: "僅於游標懸停時顯示貼文選項" showReactionsCount: "顯示貼文的反應數目" noHistory: "沒有歷史紀錄" signinHistory: "登入歷史" @@ -558,12 +558,12 @@ useObjectStorage: "使用物件儲存" objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "用於引用的 URL。如果您使用的是 CDN 或反向代理,請指定其 URL,例如 S3(https://.s3.amazonaws.com)、GCS(https://storage.googleapis.com/)。" objectStorageBucket: "儲存空間(Bucket)" -objectStorageBucketDesc: "請填寫所用服務的儲存空間(Bucket)名稱。 " +objectStorageBucketDesc: "請填寫所用服務的儲存桶(Bucket)名稱。 " objectStoragePrefix: "前綴" objectStoragePrefixDesc: "它儲存在此前綴目錄下。" objectStorageEndpoint: "端點(Endpoint)" objectStorageEndpointDesc: "如使用 AWS S3,請留空。如使用其他服務,請按照其說明文件以「」或「:」的形式設定端點(Endpoint)。" -objectStorageRegion: "地域(Region)" +objectStorageRegion: "區域(Region)" objectStorageRegionDesc: "請填寫一個分區,例如「xx-east-1」。 如果您使用的服務不設分區,請留空或填寫「us-east-1」。" objectStorageUseSSL: "使用 SSL" objectStorageUseSSLDesc: "請在不使用 https 連接 API 時關閉" @@ -586,7 +586,7 @@ popout: "彈出式視窗" volume: "音量" masterVolume: "主音量" notUseSound: "關閉音效" -useSoundOnlyWhenActive: "瀏覽器在前景運作時,Misskey 才會發出音效" +useSoundOnlyWhenActive: "僅在 Misskey 於前景運作時發出音效" details: "詳細資訊" renoteDetails: "轉發貼文的細節" chooseEmoji: "選擇您的表情符號" @@ -681,7 +681,7 @@ smtpHost: "主機" smtpPort: "埠" smtpUser: "使用者名稱" smtpPass: "密碼" -emptyToDisableSmtpAuth: "留空使用者名稱和密碼以關閉SMTP驗證。" +emptyToDisableSmtpAuth: "將使用者名稱和密碼留空以關閉 SMTP 驗證。" smtpSecure: "在 SMTP 連接中使用隱式 SSL/TLS" smtpSecureInfo: "使用 STARTTLS 時關閉。" testEmail: "測試郵件發送" @@ -711,7 +711,7 @@ useGlobalSetting: "使用全域設定" useGlobalSettingDesc: "啟用時,將使用帳戶通知設定。停用時,則可以單獨設定。" other: "其他" regenerateLoginToken: "重新產生登入權杖" -regenerateLoginTokenDescription: "重新產生用於登入的內部權杖。一般情況下是不需要這樣做的。重新產生後,所有裝置將會被登出。" +regenerateLoginTokenDescription: "重新產生用於登入的內部權杖。通常不需要使用此功能。重新產生後,所有裝置都將被登出。" theKeywordWhenSearchingForCustomEmoji: "這是搜尋自訂表情符號時的關鍵字" setMultipleBySeparatingWithSpace: "您可以使用空格分隔多個項目。" fileIdOrUrl: "檔案 ID 或 URL" @@ -745,7 +745,7 @@ unclip: "解除摘錄" confirmToUnclipAlreadyClippedNote: "此貼文已包含在摘錄「{name}」中。 你想將貼文從這個摘錄中排除嗎?" public: "公開" private: "私密" -i18nInfo: "Misskey 已被志願者們翻譯成各種語言版本。您可以瀏覽 {link} 幫助翻譯。" +i18nInfo: "Misskey 已被志願者們翻譯成各種語言版本。您可以前往 {link} 以協助翻譯。" manageAccessTokens: "管理存取權杖" accountInfo: "帳戶資訊" notesCount: "貼文數量" @@ -781,7 +781,7 @@ useSystemFont: "使用系統預設的字型" clips: "摘錄" experimentalFeatures: "實驗中的功能" experimental: "實驗性" -thisIsExperimentalFeature: "這是實驗性的功能。可能會有變更規格和不能正常動作的可能性。" +thisIsExperimentalFeature: "這是一項實驗性功能,其行為會隨需要進行調整,也可能無法正常運作。" developer: "開發者" makeExplorable: "使自己的帳戶更容易被找到" makeExplorableDescription: "如果關閉,帳戶將不會被顯示在「探索」頁面中。" @@ -1207,7 +1207,7 @@ notificationRecieveConfig: "接受通知的設定" mutualFollow: "互相追隨" followingOrFollower: "追隨中或者追隨者" fileAttachedOnly: "只顯示包含附件的貼文" -showRepliesToOthersInTimeline: "顯示給其他人的回覆" +showRepliesToOthersInTimeline: "在時間軸上顯示給其他人的回覆" hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆" showRepliesToOthersInTimelineAll: "在時間軸包含追隨中所有人的回覆" hideRepliesToOthersInTimelineAll: "在時間軸不包含追隨中所有人的回覆" @@ -1247,7 +1247,7 @@ reloadRequiredToApplySettings: "需要重新載入頁面設定才能生效。" remainingN: "剩餘:{n}" overwriteContentConfirm: "確定要覆蓋目前的內容嗎?" seasonalScreenEffect: "隨季節變換畫面的呈現" -decorate: "設置頭像裝飾" +decorate: "裝飾" addMfmFunction: "插入 MFM 功能語法" enableQuickAddMfmFunction: "顯示進階 MFM 選擇器" bubbleGame: "氣泡遊戲" @@ -1274,7 +1274,7 @@ useBackupCode: "使用備用驗證碼" launchApp: "啟動 APP" useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊" keepOriginalFilename: "保留原始檔名" -keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱會自動替換為隨機字串。" +keepOriginalFilenameDescription: "如果關閉此設定,上傳時檔案名稱會自動替換為隨機字串。" noDescription: "沒有說明文字" alwaysConfirmFollow: "追隨時總是確認" inquiry: "聯絡我們" @@ -1309,6 +1309,8 @@ availableRoles: "可用角色" acknowledgeNotesAndEnable: "了解注意事項後再開啟。" federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。" federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。" +confirmOnReact: "反應時確認" +reactAreYouSure: "用「 {emoji} 」反應嗎?" _accountSettings: requireSigninToViewContents: "須登入以顯示內容" requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。" @@ -1433,7 +1435,7 @@ _initialTutorial: useCases: "伺服器的服務條款可能會規範特定的貼文需要使用隱藏內容,除此之外也會用在隱藏劇情洩漏與敏感內容的貼文。" _howToMakeAttachmentsSensitive: title: "如何標記上傳附件為敏感內容?" - description: "如果伺服器服務條款有規範,又或者不希望上傳附件直接被看見,可以設置為「敏感內容」" + description: "如果伺服器的服務條款有規範,又或者不適合直接展示的附件,請記得加上「敏感」標記。" tryThisFile: "試試看!把附加在發文表單的圖像檔案標記為敏感內容。" _exampleNote: note: "打開納豆的包裝失敗了…" @@ -1467,7 +1469,7 @@ _serverSettings: inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。" openRegistration: "允許建立帳戶" openRegistrationWarning: "開放註冊伴隨著風險。 建議只有在伺服器受到持續監控,並準備好在出現問題時能立即處理的情況下才開放註冊。" - thisSettingWillAutomaticallyOffWhenModeratorsInactive: "為了防止 spam,如果一段期間內沒有偵測到審查員的活動,此設定將自動關閉。" + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "如果在一段期間內沒有偵測到任何審查員活動,此設定將自動關閉,以防止垃圾內容。" _accountMigration: moveFrom: "從其他帳戶遷移到這個帳戶" moveFromSub: "為另一個帳戶建立別名" @@ -1481,7 +1483,7 @@ _accountMigration: startMigration: "遷移" migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外,請確認在要遷移到的帳戶已經建立了一個別名。" movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。" - postMigrationNote: "將在完成遷移後的 24 小時取消追隨所有帳號。\n此帳戶的追隨中/追隨者人數將歸零。由於不會解除粉絲對您的追隨,因此他們仍然可以繼續閱覽此帳戶僅對追隨者公開的貼文。" + postMigrationNote: "將在完成遷移的 24 小時後取消追隨所有帳號。\n此帳戶的追隨中/追隨者人數將歸零。由於不會解除粉絲對您的追隨,因此他們仍然可以繼續閱覽此帳戶內僅對追隨者公開的貼文。" movedTo: "要遷移到的帳戶:" _achievements: earnedAt: "獲得日期" @@ -1798,7 +1800,7 @@ _role: canHideAds: "不顯示廣告" canSearchNotes: "可否搜尋貼文" canUseTranslator: "使用翻譯功能" - avatarDecorationLimit: "頭像裝飾的最大設置量" + avatarDecorationLimit: "頭像可掛上的最大裝飾數量" canImportAntennas: "允許匯入天線" canImportBlocking: "允許匯入封鎖名單" canImportFollowing: "允許匯入追隨名單" @@ -1957,7 +1959,7 @@ _instanceMute: instanceMuteDescription: "包括對被靜音伺服器上的使用者的回覆,被設定的伺服器上所有貼文及轉發都會被靜音。" instanceMuteDescription2: "設定時以換行進行分隔" title: "將隱藏被設定的伺服器貼文。" - heading: "將伺服器靜音" + heading: "要靜音的伺服器" _theme: explore: "探索佈景主題" install: "安裝佈景主題" @@ -2408,7 +2410,7 @@ _pages: note: "嵌式貼文" _note: id: "貼文ID" - idDescription: "您也可以粘貼筆記 URL 並進行設置。 " + idDescription: "您也可以貼上貼文 URL 來進行設定。 " detailed: "顯示詳細內容" _relayStatus: requesting: "等待核准" @@ -2440,6 +2442,8 @@ _notification: flushNotification: "重置通知歷史紀錄" exportOfXCompleted: "{x} 的匯出已完成。" login: "已登入" + createToken: "已產生存取權杖" + createTokenDescription: "如果您不知道,請透過「{text}」刪除存取權杖。" _types: all: "全部 " note: "使用者的最新貼文" @@ -2655,7 +2659,7 @@ _dataSaver: _hemisphere: N: "北半球" S: "南半球" - caption: "在某些客戶端的設定中,用於判斷季節。" + caption: "某些客戶端的設定會用此來判斷季節。" _reversi: reversi: "黑白棋" gameSettings: "對弈設定" @@ -2822,8 +2826,6 @@ _remoteLookupErrors: _responseInvalid: title: "回應不正確" description: "雖然能夠與這個伺服器通訊,但是取得的資料不正確。" - _responseInvalidIdHostNotMatch: - description: "輸入的 URI 的網域與最終取得的 URI 的網域不同。 如果您是透過第三方伺服器查詢遠端內容,請使用可在原始伺服器上取得的 URI 再次查詢。" _noSuchObject: title: "查無項目" description: "無法找到所要求的資源,請再次檢查 URI。" @@ -2840,3 +2842,19 @@ _captcha: _unknown: title: "CAPTCHA 錯誤" text: "發生了意外的錯誤。" +_bootErrors: + title: "載入失敗" + serverError: "如果稍等片刻並重新載入後問題仍然存在,請聯絡您的伺服器管理員並提供以下的錯誤 ID。" + solution: "執行以下操作或許可以解決問題。" + solution1: "將瀏覽器和作業系統更新至最新版本" + solution2: "停用廣告攔截器" + solution3: "清除瀏覽器的快取" + solution4: "(Tor 瀏覽器)將 dom.webaudio.enabled 設為 true" + otherOption: "其他選項" + otherOption1: "刪除用戶端設定和快取" + otherOption2: "啟動簡易用戶端" + otherOption3: "啟動修復工具" +_search: + searchScopeAll: "全部" + searchScopeLocal: "本地" + searchScopeUser: "指定使用者" diff --git a/package.json b/package.json index 5962abd95295..201b30ec3604 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "2025.2.0-vrtl.1", + "version": "2025.2.1", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@9.6.0", + "packageManager": "pnpm@9.15.4", "workspaces": [ "packages/frontend-shared", "packages/frontend", @@ -47,35 +47,35 @@ "cleanall": "pnpm clean-all" }, "resolutions": { - "chokidar": "3.5.3", + "chokidar": "3.6.0", "lodash": "4.17.21" }, "dependencies": { - "cssnano": "6.1.2", + "cssnano": "7.0.6", "execa": "8.0.1", - "fast-glob": "3.3.2", + "fast-glob": "3.3.3", "ignore-walk": "6.0.5", "js-yaml": "4.1.0", - "postcss": "8.4.49", + "postcss": "8.5.2", "tar": "6.2.1", - "terser": "5.36.0", - "typescript": "5.6.3", - "esbuild": "0.24.0", - "glob": "11.0.0" + "terser": "5.39.0", + "typescript": "5.7.3", + "esbuild": "0.25.0", + "glob": "11.0.1" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "2.0.3", - "@types/node": "22.9.0", - "@typescript-eslint/eslint-plugin": "7.17.0", - "@typescript-eslint/parser": "7.17.0", + "@misskey-dev/eslint-plugin": "2.1.0", + "@types/node": "22.13.4", + "@typescript-eslint/eslint-plugin": "8.24.0", + "@typescript-eslint/parser": "8.24.0", "cross-env": "7.0.3", - "cypress": "13.15.2", - "eslint": "9.14.0", - "globals": "15.12.0", + "cypress": "14.0.3", + "eslint": "9.20.1", + "globals": "15.15.0", "ncp": "2.0.0", - "start-server-and-test": "2.0.8" + "start-server-and-test": "2.0.10" }, "optionalDependencies": { - "@tensorflow/tfjs-core": "4.4.0" + "@tensorflow/tfjs-core": "4.22.0" } } diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc index 845190b5f419..f4bf7a4d2ada 100644 --- a/packages/backend/.swcrc +++ b/packages/backend/.swcrc @@ -1,5 +1,5 @@ { - "$schema": "https://json.schemastore.org/swcrc", + "$schema": "https://swc.rs/schema.json", "jsc": { "parser": { "syntax": "typescript", diff --git a/packages/backend/migration/1739006797620-GoogleAnalytics.js b/packages/backend/migration/1739006797620-GoogleAnalytics.js new file mode 100644 index 000000000000..5871bf098a3c --- /dev/null +++ b/packages/backend/migration/1739006797620-GoogleAnalytics.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class GoogleAnalytics1739006797620 { + name = 'GoogleAnalytics1739006797620' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "googleAnalyticsMeasurementId" character varying(64)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "googleAnalyticsMeasurementId"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 757912755ac9..cee5c7205bef 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,20 +37,20 @@ }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.3.56", - "@swc/core-darwin-x64": "1.3.56", + "@swc/core-darwin-arm64": "1.10.16", + "@swc/core-darwin-x64": "1.10.16", "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.3.56", - "@swc/core-linux-arm64-gnu": "1.3.56", - "@swc/core-linux-arm64-musl": "1.3.56", - "@swc/core-linux-x64-gnu": "1.3.56", - "@swc/core-linux-x64-musl": "1.3.56", - "@swc/core-win32-arm64-msvc": "1.3.56", - "@swc/core-win32-ia32-msvc": "1.3.56", - "@swc/core-win32-x64-msvc": "1.3.56", - "@tensorflow/tfjs": "4.4.0", - "@tensorflow/tfjs-node": "4.4.0", - "bufferutil": "4.0.7", + "@swc/core-linux-arm-gnueabihf": "1.10.16", + "@swc/core-linux-arm64-gnu": "1.10.16", + "@swc/core-linux-arm64-musl": "1.10.16", + "@swc/core-linux-x64-gnu": "1.10.16", + "@swc/core-linux-x64-musl": "1.10.16", + "@swc/core-win32-arm64-msvc": "1.10.16", + "@swc/core-win32-ia32-msvc": "1.10.16", + "@swc/core-win32-x64-msvc": "1.10.16", + "@tensorflow/tfjs": "4.22.0", + "@tensorflow/tfjs-node": "4.22.0", + "bufferutil": "4.0.9", "slacc-android-arm-eabi": "0.0.10", "slacc-android-arm64": "0.0.10", "slacc-darwin-arm64": "0.0.10", @@ -64,37 +64,37 @@ "slacc-linux-x64-musl": "0.0.10", "slacc-win32-arm64-msvc": "0.0.10", "slacc-win32-x64-msvc": "0.0.10", - "utf-8-validate": "6.0.3" + "utf-8-validate": "6.0.5" }, "dependencies": { - "@aws-sdk/client-s3": "3.620.0", - "@aws-sdk/lib-storage": "3.620.0", - "@bull-board/api": "6.5.0", - "@bull-board/fastify": "6.5.0", - "@bull-board/ui": "6.5.0", + "@aws-sdk/client-s3": "3.749.0", + "@aws-sdk/lib-storage": "3.749.0", + "@bull-board/api": "6.7.7", + "@bull-board/fastify": "6.7.7", + "@bull-board/ui": "6.7.7", "@discordapp/twemoji": "15.1.0", - "@fastify/accepts": "5.0.1", - "@fastify/cookie": "11.0.1", - "@fastify/cors": "10.0.1", - "@fastify/express": "4.0.1", - "@fastify/http-proxy": "10.0.1", - "@fastify/multipart": "9.0.1", - "@fastify/static": "8.0.2", - "@fastify/view": "10.0.1", + "@fastify/accepts": "5.0.2", + "@fastify/cookie": "11.0.2", + "@fastify/cors": "10.0.2", + "@fastify/express": "4.0.2", + "@fastify/http-proxy": "10.0.2", + "@fastify/multipart": "9.0.3", + "@fastify/static": "8.1.0", + "@fastify/view": "10.0.2", "@misskey-dev/sharp-read-bmp": "1.2.0", - "@misskey-dev/summaly": "5.1.0", - "@napi-rs/canvas": "0.1.56", - "@nestjs/common": "10.4.7", - "@nestjs/core": "10.4.7", - "@nestjs/testing": "10.4.7", + "@misskey-dev/summaly": "5.2.0", + "@napi-rs/canvas": "0.1.67", + "@nestjs/common": "11.0.9", + "@nestjs/core": "11.0.9", + "@nestjs/testing": "11.0.9", "@peertube/http-signature": "1.7.0", - "@sentry/node": "8.38.0", - "@sentry/profiling-node": "8.38.0", - "@simplewebauthn/server": "10.0.1", - "@sinonjs/fake-timers": "11.2.2", + "@sentry/node": "8.55.0", + "@sentry/profiling-node": "8.55.0", + "@simplewebauthn/server": "12.0.0", + "@sinonjs/fake-timers": "11.3.1", "@smithy/node-http-handler": "2.5.0", - "@swc/cli": "0.3.12", - "@swc/core": "1.9.2", + "@swc/cli": "0.6.0", + "@swc/core": "1.10.16", "@twemoji/parser": "15.1.1", "accepts": "1.3.8", "ajv": "8.17.1", @@ -103,10 +103,10 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.3", - "bullmq": "5.26.1", + "bullmq": "5.41.1", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", - "chalk": "5.3.0", + "chalk": "5.4.1", "chalk-template": "1.1.0", "chokidar": "3.6.0", "cli-highlight": "2.1.11", @@ -114,46 +114,46 @@ "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "5.0.0", + "fastify": "5.2.1", "fastify-raw-body": "5.0.0", "feed": "4.2.2", "file-type": "19.6.0", "fluent-ffmpeg": "2.1.3", - "form-data": "4.0.1", - "got": "14.4.4", - "happy-dom": "15.11.4", + "form-data": "4.0.2", + "got": "14.4.6", + "happy-dom": "16.8.1", "hpagent": "1.2.0", "htmlescape": "1.1.1", "http-link-header": "1.1.3", - "ioredis": "5.4.1", + "ioredis": "5.5.0", "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", "is-svg": "5.1.0", "js-yaml": "4.1.0", - "jsdom": "24.1.1", + "jsdom": "26.0.0", "json5": "2.2.3", - "jsonld": "8.3.2", + "jsonld": "8.3.3", "jsrsasign": "11.1.0", "juice": "11.0.0", - "meilisearch": "0.45.0", + "meilisearch": "0.48.2", "mfm-js": "0.24.0", "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", "ms": "3.0.0-canary.1", - "nanoid": "5.0.8", + "nanoid": "5.1.0", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.9.16", + "nodemailer": "6.10.0", "nsfwjs": "4.2.0", "oauth": "0.10.0", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.3.4", + "otpauth": "9.3.6", "parse5": "7.2.1", - "pg": "8.13.1", + "pg": "8.13.3", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -167,19 +167,19 @@ "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.1", - "sanitize-html": "2.13.1", - "secure-json-parse": "2.7.0", + "sanitize-html": "2.14.0", + "secure-json-parse": "3.0.2", "sharp": "0.33.5", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.23.5", + "systeminformation": "5.25.11", "tinycolor2": "1.6.0", "tmp": "0.2.3", "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", "typeorm": "0.3.20", - "typescript": "5.6.3", + "typescript": "5.7.3", "ulid": "2.3.0", "vary": "1.1.2", "web-push": "3.6.7", @@ -188,8 +188,8 @@ }, "devDependencies": { "@jest/globals": "29.7.0", - "@nestjs/platform-express": "10.4.7", - "@simplewebauthn/types": "10.0.0", + "@nestjs/platform-express": "10.4.15", + "@simplewebauthn/types": "12.0.0", "@swc/jest": "0.2.37", "@types/accepts": "1.3.7", "@types/archiver": "6.0.3", @@ -204,15 +204,15 @@ "@types/js-yaml": "4.0.9", "@types/jsdom": "21.1.7", "@types/jsonld": "1.5.15", - "@types/jsrsasign": "10.5.14", + "@types/jsrsasign": "10.5.15", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "22.9.0", - "@types/nodemailer": "6.4.16", + "@types/node": "22.13.4", + "@types/nodemailer": "6.4.17", "@types/oauth": "0.9.6", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.11.10", + "@types/pg": "8.11.11", "@types/pug": "2.0.10", "@types/qrcode": "1.5.5", "@types/random-seed": "0.3.5", @@ -226,18 +226,18 @@ "@types/tmp": "0.2.6", "@types/vary": "1.1.3", "@types/web-push": "3.6.4", - "@types/ws": "8.5.13", - "@typescript-eslint/eslint-plugin": "7.17.0", - "@typescript-eslint/parser": "7.17.0", - "aws-sdk-client-mock": "4.0.1", + "@types/ws": "8.5.14", + "@typescript-eslint/eslint-plugin": "8.24.0", + "@typescript-eslint/parser": "8.24.0", + "aws-sdk-client-mock": "4.1.0", "cross-env": "7.0.3", - "eslint-plugin-import": "2.30.0", + "eslint-plugin-import": "2.31.0", "execa": "8.0.1", "fkill": "9.0.0", "jest": "29.7.0", "jest-mock": "29.7.0", - "nodemon": "3.1.7", - "pid-port": "1.0.0", + "nodemon": "3.1.9", + "pid-port": "1.0.2", "simple-oauth2": "5.1.0" } } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c0b148480411..32ea7007480e 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -73,6 +73,7 @@ type Source = { proxyBypassHosts?: string[]; allowedPrivateNetworks?: string[]; + disallowExternalApRedirect?: boolean; maxFileSize?: number; @@ -105,8 +106,8 @@ type Source = { logging?: { sql?: { - disableQueryTruncation? : boolean, - enableQueryParamLogging? : boolean, + disableQueryTruncation?: boolean, + enableQueryParamLogging?: boolean, } } }; @@ -149,6 +150,7 @@ export type Config = { proxySmtp: string | undefined; proxyBypassHosts: string[] | undefined; allowedPrivateNetworks: string[] | undefined; + disallowExternalApRedirect: boolean; maxFileSize: number; clusterLimit: number | undefined; id: string; @@ -166,8 +168,8 @@ export type Config = { signToActivityPubGet: boolean | undefined; logging?: { sql?: { - disableQueryTruncation? : boolean, - enableQueryParamLogging? : boolean, + disableQueryTruncation?: boolean, + enableQueryParamLogging?: boolean, } } @@ -287,6 +289,7 @@ export function loadConfig(): Config { proxySmtp: config.proxySmtp, proxyBypassHosts: config.proxyBypassHosts, allowedPrivateNetworks: config.allowedPrivateNetworks, + disallowExternalApRedirect: config.disallowExternalApRedirect ?? false, maxFileSize: config.maxFileSize ?? 262144000, clusterLimit: config.clusterLimit, outgoingAddress: config.outgoingAddress, diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 8c7f66236e27..ee081f29b074 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -43,7 +43,7 @@ export type CaptchaSetting = { siteKey: string | null; secretKey: string | null; } -} +}; export class CaptchaError extends Error { public readonly code: CaptchaErrorCode; @@ -59,11 +59,11 @@ export class CaptchaError extends Error { export type CaptchaSaveSuccess = { success: true; -} +}; export type CaptchaSaveFailure = { success: false; error: CaptchaError; -} +}; export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure; type CaptchaResponse = { diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 2e78e6d87773..a2b74d1ab23f 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -60,8 +60,8 @@ export class DownloadService { request: operationTimeout, // whole operation timeout }, agent: { - http: this.httpRequestService.httpAgent, - https: this.httpRequestService.httpsAgent, + http: this.httpRequestService.getAgentForHttp(urlObj, true), + https: this.httpRequestService.getAgentForHttps(urlObj, true), }, http2: false, // default retry: { diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index c332e5a0a8f3..1550fe3d3c73 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -173,7 +173,8 @@ export class DriveService { ?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`; // for original - const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`; + const prefix = this.meta.objectStoragePrefix ? `${this.meta.objectStoragePrefix}/` : ''; + const key = `${prefix}${randomUUID()}${ext}`; const url = `${ baseUrl }/${ key }`; // for alts @@ -190,7 +191,7 @@ export class DriveService { ]; if (alts.webpublic) { - webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; + webpublicKey = `${prefix}webpublic-${randomUUID()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); @@ -198,7 +199,7 @@ export class DriveService { } if (alts.thumbnail) { - thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; + thumbnailKey = `${prefix}thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index da198d0e4200..45d7ea11e451 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -164,6 +164,13 @@ export class EmailService { available: boolean; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; }> { + if (!this.utilityService.validateEmailFormat(emailAddress)) { + return { + available: false, + reason: 'format', + }; + } + const exist = await this.userProfilesRepository.countBy({ emailVerified: true, email: emailAddress, diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index be3c968eeb67..3cd5cb98b9fb 100644 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -9,7 +9,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; -export type FanoutTimelineName = +export type FanoutTimelineName = ( // home timeline | `homeTimeline:${string}` | `homeTimelineWithFiles:${string}` // only notes with files are included @@ -43,6 +43,7 @@ export type FanoutTimelineName = | 'vmimiRelayTimelineWithFiles' // only non-reply notes with files are included | 'vmimiRelayTimelineWithReplies' // only replies are included | `vmimiRelayTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id. +); @Injectable() export class FanoutTimelineService { diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 03646ff56680..224fdabc4c50 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -211,7 +211,7 @@ type SerializedAll = { type UndefinedAsNullAll = { [K in keyof T]: T[K] extends undefined ? null : T[K]; -} +}; export interface InternalEventTypes { userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 083153940a65..13d8f7f43be4 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -16,7 +16,7 @@ import type { Config } from '@/config.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js'; +import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; import type { IObject } from '@/core/activitypub/type.js'; import type { Response } from 'node-fetch'; import type { URL } from 'node:url'; @@ -115,32 +115,32 @@ export class HttpRequestService { /** * Get http non-proxy agent (without local address filtering) */ - private httpNative: http.Agent; + private readonly httpNative: http.Agent; /** * Get https non-proxy agent (without local address filtering) */ - private httpsNative: https.Agent; + private readonly httpsNative: https.Agent; /** * Get http non-proxy agent */ - private http: http.Agent; + private readonly http: http.Agent; /** * Get https non-proxy agent */ - private https: https.Agent; + private readonly https: https.Agent; /** * Get http proxy or non-proxy agent */ - public httpAgent: http.Agent; + public readonly httpAgent: http.Agent; /** * Get https proxy or non-proxy agent */ - public httpsAgent: https.Agent; + public readonly httpsAgent: https.Agent; constructor( @Inject(DI.config) @@ -197,7 +197,8 @@ export class HttpRequestService { /** * Get agent by URL * @param url URL - * @param bypassProxy Allways bypass proxy + * @param bypassProxy Always bypass proxy + * @param isLocalAddressAllowed */ @bindThis public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent { @@ -214,8 +215,40 @@ export class HttpRequestService { } } + /** + * Get agent for http by URL + * @param url URL + * @param isLocalAddressAllowed + */ + @bindThis + public getAgentForHttp(url: URL, isLocalAddressAllowed = false): http.Agent { + if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) { + return isLocalAddressAllowed + ? this.httpNative + : this.http; + } else { + return this.httpAgent; + } + } + + /** + * Get agent for https by URL + * @param url URL + * @param isLocalAddressAllowed + */ + @bindThis + public getAgentForHttps(url: URL, isLocalAddressAllowed = false): https.Agent { + if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) { + return isLocalAddressAllowed + ? this.httpsNative + : this.https; + } else { + return this.httpsAgent; + } + } + @bindThis - public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise { + public async getActivityJson(url: string, isLocalAddressAllowed = false, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise { const res = await this.send(url, { method: 'GET', headers: { @@ -232,7 +265,7 @@ export class HttpRequestService { const finalUrl = res.url; // redirects may have been involved const activity = await res.json() as IObject; - assertActivityMatchesUrls(activity, [finalUrl]); + assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail); return activity; } diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index bf06d4457e2a..00208927e2e6 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -492,7 +492,8 @@ export class MfmService { appendChildren(nodes, body); - const serialized = new XMLSerializer().serializeToString(body); + // Remove the unnecessary namespace + const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*

/, '

'); happyDOM.close().catch(err => {}); diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 4ecd2592b2ca..e394506a44a0 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Brackets, In } from 'typeorm'; +import { Brackets, In, IsNull, Not } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; @@ -189,13 +189,27 @@ export class NoteDeleteService { }) as MiRemoteUser[]; } + @bindThis + private async getRenotedOrRepliedRemoteUsers(note: MiNote) { + const query = this.notesRepository.createQueryBuilder('note') + .leftJoinAndSelect('note.user', 'user') + .where(new Brackets(qb => { + qb.orWhere('note.renoteId = :renoteId', { renoteId: note.id }); + qb.orWhere('note.replyId = :replyId', { replyId: note.id }); + })) + .andWhere({ userHost: Not(IsNull()) }); + const notes = await query.getMany() as (MiNote & { user: MiRemoteUser })[]; + const remoteUsers = notes.map(({ user }) => user); + return remoteUsers; + } + @bindThis private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); - const remoteUsers = await this.getMentionedRemoteUsers(note); - for (const remoteUser of remoteUsers) { - this.apDeliverManagerService.deliverToUser(user, content, remoteUser); - } + this.apDeliverManagerService.deliverToUsers(user, content, [ + ...await this.getMentionedRemoteUsers(note), + ...await this.getRenotedOrRepliedRemoteUsers(note), + ]); } } diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index 098b5e170618..a2f1b73cdbd6 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -74,7 +74,7 @@ export class RemoteUserResolveService { if (user == null) { const self = await this.resolveSelf(acctLower); - if (self.href.startsWith(this.config.url)) { + if (this.utilityService.isUriLocal(self.href)) { const local = this.apDbResolverService.parseUri(self.href); if (local.local && local.type === 'users') { // the LR points to local diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 64e3f2f56a8e..bc62559e4679 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -220,7 +220,7 @@ export class SearchService { .leftJoinAndSelect('renote.user', 'renoteUser'); if (this.config.fulltextSearch?.provider === 'sqlPgroonga') { - query.andWhere('note.text &@ :q', { q }); + query.andWhere('note.text &@~ :q', { q }); } else { query.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` }); } diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts index b1728671aec1..9b0a598a1ba2 100644 --- a/packages/backend/src/core/UserWebhookService.ts +++ b/packages/backend/src/core/UserWebhookService.ts @@ -15,7 +15,7 @@ import { QueueService } from '@/core/QueueService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; export type UserWebhookPayload = - T extends 'note' | 'reply' | 'renote' |'mention' ? { + T extends 'note' | 'reply' | 'renote' | 'mention' ? { note: Packed<'Note'>, } : T extends 'follow' | 'unfollow' ? { diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index fcb750d3bf7d..23fb928ac9bf 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -38,6 +38,14 @@ export class UtilityService { return this.punyHost(uri) === this.toPuny(this.config.host); } + // メールアドレスのバリデーションを行う + // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + @bindThis + public validateEmailFormat(email: string): boolean { + const regexp = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + return regexp.test(email); + } + @bindThis public isBlockedHost(blockedHosts: string[], host: string | null): boolean { if (host == null) return false; diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index ed75e4f46793..372e1e2ab714 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -127,11 +127,11 @@ export class WebAuthnService { const { registrationInfo } = verification; return { - credentialID: registrationInfo.credentialID, - credentialPublicKey: registrationInfo.credentialPublicKey, + credentialID: registrationInfo.credential.id, + credentialPublicKey: registrationInfo.credential.publicKey, attestationObject: registrationInfo.attestationObject, fmt: registrationInfo.fmt, - counter: registrationInfo.counter, + counter: registrationInfo.credential.counter, userVerified: registrationInfo.userVerified, credentialDeviceType: registrationInfo.credentialDeviceType, credentialBackedUp: registrationInfo.credentialBackedUp, @@ -212,9 +212,9 @@ export class WebAuthnService { expectedChallenge: challenge, expectedOrigin: relyingParty.origin, expectedRPID: relyingParty.rpId, - authenticator: { - credentialID: key.id, - credentialPublicKey: Buffer.from(key.publicKey, 'base64url'), + credential: { + id: key.id, + publicKey: Buffer.from(key.publicKey, 'base64url'), counter: key.counter, transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, }, @@ -292,9 +292,9 @@ export class WebAuthnService { expectedChallenge: challenge, expectedOrigin: relyingParty.origin, expectedRPID: relyingParty.rpId, - authenticator: { - credentialID: key.id, - credentialPublicKey: Buffer.from(key.publicKey, 'base64url'), + credential: { + id: key.id, + publicKey: Buffer.from(key.publicKey, 'base64url'), counter: key.counter, transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, }, diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 5d07cd8e8f63..0140ce9fd63b 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -196,6 +196,25 @@ export class ApDeliverManagerService { await manager.execute(); } + /** + * Deliver activity to users + * @param actor + * @param activity Activity + * @param targets Target users + */ + @bindThis + public async deliverToUsers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, targets: MiRemoteUser[]): Promise { + const manager = new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + actor, + activity, + ); + for (const to of targets) manager.addDirectRecipe(to); + await manager.execute(); + } + @bindThis public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager { return new DeliverManager( diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 914809506743..8688015afffe 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -27,6 +27,7 @@ import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFil import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { IdService } from '@/core/IdService.js'; +import { UtilityService } from '@/core/UtilityService.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; @@ -61,6 +62,7 @@ export class ApRendererService { private apMfmService: ApMfmService, private mfmService: MfmService, private idService: IdService, + private utilityService: UtilityService, ) { } @@ -577,7 +579,7 @@ export class ApRendererService { @bindThis public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo { - const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; + const id = typeof object !== 'string' && typeof object.id === 'string' && this.utilityService.isUriLocal(object.id) ? `${object.id}/undo` : undefined; return { type: 'Undo', diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 8c3b7295e4a8..6c29cce32555 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js'; +import { assertActivityMatchesUrls, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; import type { IObject } from './type.js'; type Request = { @@ -185,7 +185,7 @@ export class ApRequestService { * @param url URL to fetch */ @bindThis - public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise { + public async signedGet(url: string, user: { id: MiUser['id'] }, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict, followAlternate?: boolean): Promise { const _followAlternate = followAlternate ?? true; const keypair = await this.userKeypairService.getUserKeypair(user.id); @@ -243,7 +243,7 @@ export class ApRequestService { if (alternate) { const href = alternate.getAttribute('href'); if (href && this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) { - return await this.signedGet(href, user, false); + return await this.signedGet(href, user, allowSoftfail, false); } } } catch (e) { @@ -258,7 +258,7 @@ export class ApRequestService { const finalUrl = res.url; // redirects may have been involved const activity = await res.json() as IObject; - assertActivityMatchesUrls(activity, [finalUrl]); + assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail); return activity; } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 52cc56914013..fb963294cb89 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -21,6 +21,7 @@ import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { FetchAllowSoftFailMask } from './misc/check-against-url.js'; export class Resolver { private history: Set; @@ -72,7 +73,7 @@ export class Resolver { } @bindThis - public async resolve(value: string | IObject): Promise { + public async resolve(value: string | IObject, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise { if (typeof value !== 'string') { return value; } @@ -108,8 +109,8 @@ export class Resolver { } const object = (this.user - ? await this.apRequestService.signedGet(value, this.user) as IObject - : await this.httpRequestService.getActivityJson(value)) as IObject; + ? await this.apRequestService.signedGet(value, this.user, allowSoftfail) as IObject + : await this.httpRequestService.getActivityJson(value, undefined, allowSoftfail)) as IObject; if ( Array.isArray(object['@context']) ? @@ -118,19 +119,7 @@ export class Resolver { ) { throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response'); } - - // HttpRequestService / ApRequestService have already checked that - // `object.id` or `object.url` matches the URL used to fetch the - // object after redirects; here we double-check that no redirects - // bounced between hosts - if (object.id == null) { - throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', 'invalid AP object: missing id'); - } - - if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) { - throw new IdentifiableError('fd93c2fa-69a8-440f-880b-bf178e0ec877', `invalid AP object ${value}: id ${object.id} has different host`); - } - + return object; } diff --git a/packages/backend/src/core/activitypub/misc/check-against-url.ts b/packages/backend/src/core/activitypub/misc/check-against-url.ts index d679bd81804a..dfcfb1943e85 100644 --- a/packages/backend/src/core/activitypub/misc/check-against-url.ts +++ b/packages/backend/src/core/activitypub/misc/check-against-url.ts @@ -4,18 +4,124 @@ */ import type { IObject } from '../type.js'; -export function assertActivityMatchesUrls(activity: IObject, urls: string[]) { - const hosts = urls.map(it => new URL(it).host); +export enum FetchAllowSoftFailMask { + // Allow no softfail flags + Strict = 0, + // The values in tuple (requestUrl, finalUrl, objectId) are not all identical + // + // This condition is common for user-initiated lookups but should not be allowed in federation loop + // + // Allow variations: + // good example: https://alice.example.com/@user -> https://alice.example.com/user/:userId + // problematic example: https://alice.example.com/redirect?url=https://bad.example.com/ -> https://bad.example.com/ -> https://alice.example.com/somethingElse + NonCanonicalId = 1 << 0, + // Allow the final object to be at most one subdomain deeper than the request URL, similar to SPF relaxed alignment + // + // Currently no code path allows this flag to be set, but is kept in case of future use as some niche deployments do this, and we provide a pre-reviewed mechanism to opt-in. + // + // Allow variations: + // good example: https://example.com/@user -> https://activitypub.example.com/@user { id: 'https://activitypub.example.com/@user' } + // problematic example: https://example.com/@user -> https://untrusted.example.com/@user { id: 'https://untrusted.example.com/@user' } + MisalignedOrigin = 1 << 1, + // The requested URL has a different host than the returned object ID, although the final URL is still consistent with the object ID + // + // This condition is common for user-initiated lookups using an intermediate host but should not be allowed in federation loops + // + // Allow variations: + // good example: https://alice.example.com/@user@bob.example.com -> https://bob.example.com/@user { id: 'https://bob.example.com/@user' } + // problematic example: https://alice.example.com/definitelyAlice -> https://bob.example.com/@somebodyElse { id: 'https://bob.example.com/@somebodyElse' } + CrossOrigin = 1 << 2 | MisalignedOrigin, + // Allow all softfail flags + // + // do not use this flag on released code + Any = ~0, +} + +/** + * Fuzz match on whether the candidate host has authority over the request host + * + * @param requestHost The host of the requested resources + * @param candidateHost The host of final response + * @returns Whether the candidate host has authority over the request host, or if a soft fail is required for a match + */ +function hostFuzzyMatch(requestHost: string, candidateHost: string): FetchAllowSoftFailMask { + const requestFqdn = requestHost.endsWith('.') ? requestHost : `${requestHost}.`; + const candidateFqdn = candidateHost.endsWith('.') ? candidateHost : `${candidateHost}.`; + + if (requestFqdn === candidateFqdn) { + return FetchAllowSoftFailMask.Strict; + } + + // allow only one case where candidateHost is a first-level subdomain of requestHost + const requestDnsDepth = requestFqdn.split('.').length; + const candidateDnsDepth = candidateFqdn.split('.').length; + + if ((candidateDnsDepth - requestDnsDepth) !== 1) { + return FetchAllowSoftFailMask.CrossOrigin; + } - const idOk = activity.id !== undefined && hosts.includes(new URL(activity.id).host); + if (`.${candidateHost}`.endsWith(`.${requestHost}`)) { + return FetchAllowSoftFailMask.MisalignedOrigin; + } - // technically `activity.url` could be an `ApObject = IObject | - // string | (IObject | string)[]`, but if it's a complicated thing - // and the `activity.id` doesn't match, I think we're fine - // rejecting the activity - const urlOk = typeof(activity.url) === 'string' && hosts.includes(new URL(activity.url).host); + return FetchAllowSoftFailMask.CrossOrigin; +} - if (!idOk && !urlOk) { - throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${activity?.url}) match location(${urls})`); +// normalize host names by removing www. prefix +function normalizeSynonymousSubdomain(url: URL | string): URL { + const urlParsed = url instanceof URL ? url : new URL(url); + const host = urlParsed.host; + const normalizedHost = host.replace(/^www\./, ''); + return new URL(urlParsed.toString().replace(host, normalizedHost)); +} + +export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IObject, candidateUrls: (string | URL)[], allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask { + // must have a unique identifier to verify authority + if (!activity.id) { + throw new Error('bad Activity: missing id field'); } + + let softfail = 0; + + // if the flag is allowed, set the flag on return otherwise throw + const requireSoftfail = (needed: FetchAllowSoftFailMask, message: string) => { + if ((allowSoftfail & needed) !== needed) { + throw new Error(message); + } + + softfail |= needed; + }; + + const requestUrlParsed = normalizeSynonymousSubdomain(requestUrl); + const idParsed = normalizeSynonymousSubdomain(activity.id); + + const candidateUrlsParsed = candidateUrls.map(it => normalizeSynonymousSubdomain(it)); + + const requestUrlSecure = requestUrlParsed.protocol === 'https:'; + const finalUrlSecure = candidateUrlsParsed.every(it => it.protocol === 'https:'); + if (requestUrlSecure && !finalUrlSecure) { + throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`); + } + + // Compare final URL to the ID + if (!candidateUrlsParsed.some(it => it.href === idParsed.href)) { + requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${candidateUrlsParsed.map(it => it.toString())})`); + + // at lease host need to match exactly (ActivityPub requirement) + if (!candidateUrlsParsed.some(it => idParsed.host === it.host)) { + throw new Error(`bad Activity: id(${activity.id}) does not match response host(${candidateUrlsParsed.map(it => it.host)})`); + } + } + + // Compare request URL to the ID + if (!requestUrlParsed.href.includes(idParsed.href)) { + requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match request url(${requestUrlParsed.toString()})`); + + // if cross-origin lookup is allowed, we can accept some variation between the original request URL to the final object ID (but not between the final URL and the object ID) + const hostResult = hostFuzzyMatch(requestUrlParsed.host, idParsed.host); + + requireSoftfail(hostResult, `bad Activity: id(${activity.id}) is valid but is not the same origin as request url(${requestUrlParsed.toString()})`); + } + + return softfail; } diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index ef20e4acf846..4b4991a3e45d 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -97,6 +97,7 @@ export class MetaEntityService { enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, enableTestcaptcha: instance.enableTestcaptcha, + googleAnalyticsMeasurementId: instance.googleAnalyticsMeasurementId, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png', diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index d3c087a15376..fbd3892dd4f8 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -57,12 +57,14 @@ const ajv = new Ajv(); function isLocalUser(user: MiUser): user is MiLocalUser; function isLocalUser(user: T): user is (T & { host: null; }); + function isLocalUser(user: MiUser | { host: MiUser['host'] }): boolean { return user.host == null; } function isRemoteUser(user: MiUser): user is MiRemoteUser; function isRemoteUser(user: T): user is (T & { host: string; }); + function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean { return !isLocalUser(user); } @@ -78,7 +80,7 @@ export type UserRelation = { isBlocked: boolean isMuted: boolean isRenoteMuted: boolean -} +}; @Injectable() export class UserEntityService implements OnModuleInit { diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index f612591eda03..ac74d68c952e 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -143,7 +143,7 @@ type OfSchema = { readonly anyOf?: ReadonlyArray; readonly oneOf?: ReadonlyArray; readonly allOf?: ReadonlyArray; -} +}; export interface Schema extends OfSchema { readonly type?: TypeStringef; @@ -217,7 +217,7 @@ type ObjectSchemaTypeDef

= : p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md p['allOf'] extends ReadonlyArray ? UnionToIntersection> : - any + any; type ObjectSchemaType

= NullOrUndefined>; diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts index bd7fe12058a0..195f7c4d476e 100644 --- a/packages/backend/src/misc/json-value.ts +++ b/packages/backend/src/misc/json-value.ts @@ -4,7 +4,7 @@ */ export type JsonValue = JsonArray | JsonObject | string | number | boolean | null; -export type JsonObject = {[K in string]?: JsonValue}; +export type JsonObject = { [K in string]?: JsonValue }; export type JsonArray = JsonValue[]; export function isJsonObject(value: JsonValue | undefined): value is JsonObject { diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 9e07196ed8a8..89f1615949a8 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -663,4 +663,10 @@ export class MiMeta { default: '{}', }) public federationHosts: string[]; + + @Column('varchar', { + length: 64, + nullable: true, + }) + public googleAnalyticsMeasurementId: string | null; } diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index b7f8e94d691d..5772ace338fb 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -90,6 +90,10 @@ export type MiNotification = { type: 'login'; id: string; createdAt: string; +} | { + type: 'createToken'; + id: string; + createdAt: string; } | { type: 'app'; id: string; diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 96de30c4c2c9..549d78a22ceb 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -288,24 +288,24 @@ export class MiUser { export type MiLocalUser = MiUser & { host: null; uri: null; -} +}; export type MiPartialLocalUser = Partial & { id: MiUser['id']; host: null; uri: null; -} +}; export type MiRemoteUser = MiUser & { host: string; uri: string; -} +}; export type MiPartialRemoteUser = Partial & { id: MiUser['id']; host: string; uri: string; -} +}; export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; export const passwordSchema = { type: 'string', minLength: 1 } as const; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index e2e114e82f46..96bc0ed85240 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -119,6 +119,10 @@ export const packedMetaLiteSchema = { type: 'boolean', optional: false, nullable: false, }, + googleAnalyticsMeasurementId: { + type: 'string', + optional: false, nullable: true, + }, swPublickey: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index cddaf4bc8370..1638b2b3c72a 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -332,6 +332,16 @@ export const packedNotificationSchema = { enum: ['login'], }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['createToken'], + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index d09240eba1c9..8a0b7d97d72b 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -92,7 +92,7 @@ const sqlLogger = dbLogger.createSubLogger('sql', 'gray'); export type LoggerProps = { disableQueryTruncation?: boolean; enableQueryParamLogging?: boolean; -} +}; function highlightSql(sql: string) { return highlight.highlight(sql, { diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts index 2e84430e72a4..c9fe4fca7311 100644 --- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -29,7 +29,7 @@ export type ModeratorInactivityEvaluationResult = { isModeratorsInactive: boolean; inactiveModerators: MiUser[]; remainingTime: ModeratorInactivityRemainingTime; -} +}; export type ModeratorInactivityRemainingTime = { time: number; diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 004fe1382d70..079e014da83f 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -107,12 +107,12 @@ export class InboxProcessorService implements OnApplicationShutdown { // それでもわからなければ終了 if (authUser == null) { - throw new Bull.UnrecoverableError('skip: failed to resolve user'); + throw new Bull.UnrecoverableError(`skip: failed to resolve user ${getApId(activity.actor)}`); } // publicKey がなくても終了 if (authUser.key == null) { - throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey'); + throw new Bull.UnrecoverableError(`skip: failed to resolve user publicKey ${getApId(activity.actor)}`); } // HTTP-Signatureの検証 diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 5db919a1497c..757daea88b6f 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -38,7 +38,7 @@ export type RelationshipJobData = { silent?: boolean; requestId?: string; withReplies?: boolean; -} +}; export type DbJobData = DbJobMap[T]; @@ -61,11 +61,11 @@ export type DbJobMap = { importUserLists: DbUserImportJobData; importCustomEmojis: DbUserImportJobData; deleteAccount: DbUserDeleteJobData; -} +}; export type DbJobDataWithUser = { user: ThinUser; -} +}; export type DbExportFollowingData = { user: ThinUser; @@ -75,7 +75,7 @@ export type DbExportFollowingData = { export type DBExportAntennasData = { user: ThinUser -} +}; export type DbUserDeleteJobData = { user: ThinUser; @@ -91,7 +91,7 @@ export type DbUserImportJobData = { export type DBAntennaImportJobData = { user: ThinUser, antenna: Antenna -} +}; export type DbUserImportToDbJobData = { user: ThinUser; diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index fd2bd3267d62..b899053287bd 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -103,6 +103,43 @@ export class ServerService implements OnApplicationShutdown { serve: false, }); + // if the requester looks like to be performing an ActivityPub object lookup, reject all external redirects + // + // this will break lookup that involve copying a URL from a third-party server, like trying to lookup http://charlie.example.com/@alice@alice.com + // + // this is not required by standard but protect us from peers that did not validate final URL. + if (this.config.disallowExternalApRedirect) { + const maybeApLookupRegex = /application\/activity\+json|application\/ld\+json.+activitystreams/i; + fastify.addHook('onSend', (request, reply, _, done) => { + const location = reply.getHeader('location'); + if (reply.statusCode < 300 || reply.statusCode >= 400 || typeof location !== 'string') { + done(); + return; + } + + if (!maybeApLookupRegex.test(request.headers.accept ?? '')) { + done(); + return; + } + + const effectiveLocation = process.env.NODE_ENV === 'production' ? location : location.replace(/^http:\/\//, 'https://'); + if (effectiveLocation.startsWith(`https://${this.config.host}/`)) { + done(); + return; + } + + reply.status(406); + reply.removeHeader('location'); + reply.header('content-type', 'text/plain; charset=utf-8'); + reply.header('link', `<${encodeURI(location)}>; rel="canonical"`); + done(null, [ + "Refusing to relay remote ActivityPub object lookup.", + "", + `Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`, + ].join('\n')); + }); + } + fastify.register(this.apiServerService.createServer, { prefix: '/api' }); fastify.register(this.openApiServerService.createServer); fastify.register(this.fileServerService.createServer); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index a9a2ebc04128..7f4ca9c0e04d 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -122,7 +122,7 @@ export type IEndpointMeta = (Omit & { requireAdmin: true, kind: (typeof permissions)[number], -}) +}); export interface IEndpoint { name: string; diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 2adf11b687cf..5e24f3feb514 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -73,6 +73,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + googleAnalyticsMeasurementId: { + type: 'string', + optional: false, nullable: true, + }, swPublickey: { type: 'string', optional: false, nullable: true, @@ -512,6 +516,7 @@ export const meta = { }, federation: { type: 'string', + enum: ['all', 'specified', 'none'], optional: false, nullable: false, }, federationHosts: { @@ -571,6 +576,7 @@ export default class extends Endpoint { // eslint- enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, enableTestcaptcha: instance.enableTestcaptcha, + googleAnalyticsMeasurementId: instance.googleAnalyticsMeasurementId, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 0ee94e76251b..fc450dfb3777 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -84,6 +84,7 @@ export const paramDef = { turnstileSiteKey: { type: 'string', nullable: true }, turnstileSecretKey: { type: 'string', nullable: true }, enableTestcaptcha: { type: 'boolean' }, + googleAnalyticsMeasurementId: { type: 'string', nullable: true }, sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, setSensitiveFlagAutomatically: { type: 'boolean' }, @@ -117,7 +118,7 @@ export const paramDef = { useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, objectStorageBucket: { type: 'string', nullable: true }, - objectStoragePrefix: { type: 'string', nullable: true }, + objectStoragePrefix: { type: 'string', pattern: /^[a-zA-Z0-9-._]*$/.source, nullable: true }, objectStorageEndpoint: { type: 'string', nullable: true }, objectStorageRegion: { type: 'string', nullable: true }, objectStoragePort: { type: 'integer', nullable: true }, @@ -372,6 +373,12 @@ export default class extends Endpoint { // eslint- set.enableTestcaptcha = ps.enableTestcaptcha; } + if (ps.googleAnalyticsMeasurementId !== undefined) { + // 空文字列をnullにしたいので??は使わない + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + set.googleAnalyticsMeasurementId = ps.googleAnalyticsMeasurementId || null; + } + if (ps.sensitiveMediaDetection !== undefined) { set.sensitiveMediaDetection = ps.sensitiveMediaDetection; } diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 5c2e82da8823..4afed7dc5c97 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -20,6 +20,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '../../error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; export const meta = { tags: ['federation'], @@ -53,11 +54,6 @@ export const meta = { code: 'RESPONSE_INVALID', id: '70193c39-54f3-4813-82f0-70a680f7495b', }, - responseInvalidIdHostNotMatch: { - message: 'Requested URI and response URI host does not match.', - code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH', - id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a', - }, noSuchObject: { message: 'No such object.', code: 'NO_SUCH_OBJECT', @@ -153,7 +149,8 @@ export default class extends Endpoint { // eslint- // リモートから一旦オブジェクトフェッチ const resolver = this.apResolverService.createResolver(); - const object = await resolver.resolve(uri).catch((err) => { + // allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob) + const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { // resolve @@ -165,10 +162,7 @@ export default class extends Endpoint { // eslint- case '09d79f9e-64f1-4316-9cfa-e75c4d091574': throw new ApiError(meta.errors.federationNotAllowed); case '72180409-793c-4973-868e-5a118eb5519b': - case 'ad2dc287-75c1-44c4-839d-3d2e64576675': throw new ApiError(meta.errors.responseInvalid); - case 'fd93c2fa-69a8-440f-880b-bf178e0ec877': - throw new ApiError(meta.errors.responseInvalidIdHostNotMatch); // resolveLocal case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8': diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index ceebc8ba5e2d..b40706297d5e 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -39,7 +39,7 @@ export const paramDef = { properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, isPublic: { type: 'boolean', default: false }, - description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, + description: { type: 'string', nullable: true, maxLength: 2048 }, }, required: ['name'], } as const; @@ -53,7 +53,9 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { let clip: MiClip; try { - clip = await this.clipService.create(me, ps.name, ps.isPublic, ps.description ?? null); + // 空文字列をnullにしたいので??は使わない + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + clip = await this.clipService.create(me, ps.name, ps.isPublic, ps.description || null); } catch (e) { if (e instanceof ClipService.TooManyClipsError) { throw new ApiError(meta.errors.tooManyClips); diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 603a3ccf3dc5..6ff3f9aada42 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -39,7 +39,7 @@ export const paramDef = { clipId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, isPublic: { type: 'boolean' }, - description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, + description: { type: 'string', nullable: true, maxLength: 2048 }, }, required: ['clipId'], } as const; @@ -53,7 +53,9 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { try { - await this.clipService.update(me, ps.clipId, ps.name, ps.isPublic, ps.description); + // 空文字列をnullにしたいので??は使わない + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + await this.clipService.update(me, ps.clipId, ps.name, ps.isPublic, ps.description || null); } catch (e) { if (e instanceof ClipService.NoSuchClipError) { throw new ApiError(meta.errors.noSuchClip); diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 8935c2c2daec..b45d21410b06 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -96,7 +96,7 @@ export default class extends Endpoint { // eslint- await this.userFollowingService.unfollow(follower, followee); - return await this.userEntityService.pack(followee.id, me); + return await this.userEntityService.pack(follower.id, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index fc9a8f3ebebd..f1e372664112 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { AccessTokensRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DI } from '@/di-symbols.js'; @@ -50,6 +51,7 @@ export default class extends Endpoint { // eslint- private accessTokensRepository: AccessTokensRepository, private idService: IdService, + private notificationService: NotificationService, ) { super(meta, paramDef, async (ps, me) => { // Generate access token @@ -71,6 +73,9 @@ export default class extends Endpoint { // eslint- permission: ps.permission, }); + // アクセストークンが生成されたことを通知 + this.notificationService.createNotification(me.id, 'createToken', {}); + return { token: accessToken, }; diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 3b20ec1321da..ea64e32ee652 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -210,9 +210,15 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { spec.paths['/' + endpoint.name] = { ...(endpoint.meta.allowGet ? { - get: info, + get: { + ...info, + operationId: 'get___' + info.operationId, + }, } : {}), - post: info, + post: { + ...info, + operationId: 'post___' + info.operationId, + }, }; } diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 84cb552369fe..686aea423c81 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -82,8 +82,8 @@ export default abstract class Channel { this.connection = connection; } - public send(payload: { type: string, body: JsonValue }): void - public send(type: string, payload: JsonValue): void + public send(payload: { type: string, body: JsonValue }): void; + public send(type: string, payload: JsonValue): void; @bindThis public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) { const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string); @@ -108,4 +108,4 @@ export type MiChannelService = { requireCredential: T; kind: T extends true ? string : string | null | undefined; create: (id: string, connection: Connection) => Channel; -} +}; diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js index 48d1cd262bdc..9de127538095 100644 --- a/packages/backend/src/server/web/boot.embed.js +++ b/packages/backend/src/server/web/boot.embed.js @@ -114,13 +114,17 @@ if (document.readyState === 'loading') { await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); } + + const locale = JSON.parse(localStorage.getItem('locale') || '{}'); + + const title = locale?._bootErrors?.title || 'Failed to initialize Misskey'; + const reload = locale?.reload || 'Reload'; + document.body.innerHTML = ` -

読み込みに失敗しました
-
Failed to initialize Misskey
+
${title}
Error Code: ${code}
`; addStyle(` #misskey_app, diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index a04640d9935b..b55d327f86e8 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -151,6 +151,22 @@ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); } + const locale = JSON.parse(localStorage.getItem('locale') || '{}'); + + const messages = Object.assign({ + title: 'Failed to initialize Misskey', + solution: 'The following actions may solve the problem.', + solution1: 'Update your os and browser', + solution2: 'Disable an adblocker', + solution3: 'Clear the browser cache', + solution4: '(Tor Browser) Set dom.webaudio.enabled to true', + otherOption: 'Other options', + otherOption1: 'Clear preferences and cache', + otherOption2: 'Start the simple client', + otherOption3: 'Start the repair tool', + }, locale?._bootErrors || {}); + const reload = locale?.reload || 'Reload'; + let errorsElement = document.getElementById('errors'); if (!errorsElement) { @@ -160,32 +176,32 @@ -

Failed to load
読み込みに失敗しました

+

${messages.title}

-

The following actions may solve the problem. / 以下を行うと解決する可能性があります。

-

Update your os and browser / ブラウザおよびOSを最新バージョンに更新する

-

Disable an adblocker / アドブロッカーを無効にする

-

Clear the browser cache / ブラウザのキャッシュをクリアする

-

(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する

+

${messages.solution}

+

${messages.solution1}

+

${messages.solution2}

+

${messages.solution3}

+

${messages.solution4}

- Other options / その他のオプション + ${messages.otherOption}

diff --git a/packages/backend/src/server/web/error.css b/packages/backend/src/server/web/error.css index f2b63296eb5d..803bd1b4b52a 100644 --- a/packages/backend/src/server/web/error.css +++ b/packages/backend/src/server/web/error.css @@ -5,112 +5,107 @@ */ * { - font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; + font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; } #misskey_app, #splash { - display: none !important; + display: none !important; } body, html { - background-color: #222; - color: #dfddcc; - justify-content: center; - margin: auto; - padding: 10px; - text-align: center; + background-color: #222; + color: #dfddcc; + justify-content: center; + margin: auto; + padding: 10px; + text-align: center; } button { - border-radius: 999px; - padding: 0px 12px 0px 12px; - border: none; - cursor: pointer; - margin-bottom: 12px; + border-radius: 999px; + padding: 0px 12px 0px 12px; + border: none; + cursor: pointer; + margin-bottom: 12px; } .button-big { - background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0)); - line-height: 50px; + background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0)); + line-height: 50px; } .button-big:hover { - background: rgb(153, 204, 0); + background: rgb(153, 204, 0); } .button-small { - background: #444; - line-height: 40px; + background: #444; + line-height: 40px; } .button-small:hover { - background: #555; + background: #555; } .button-label-big { - color: #222; - font-weight: bold; - font-size: 20px; - padding: 12px; + color: #222; + font-weight: bold; + font-size: 1.2em; + padding: 12px; } .button-label-small { - color: rgb(153, 204, 0); - font-size: 16px; - padding: 12px; + color: rgb(153, 204, 0); + font-size: 16px; + padding: 12px; } a { - color: rgb(134, 179, 0); - text-decoration: none; + color: rgb(134, 179, 0); + text-decoration: none; } p, li { - font-size: 16px; -} - -.dont-worry, -#msg { - font-size: 18px; + font-size: 16px; } .icon-warning { - color: #dec340; - height: 4rem; - padding-top: 2rem; + color: #dec340; + height: 4rem; + padding-top: 2rem; } h1 { - font-size: 32px; + font-size: 1.5em; + margin: 1em; } code { - display: block; - font-family: Fira, FiraCode, monospace; - background: #333; - padding: 0.5rem 1rem; - max-width: 40rem; - border-radius: 10px; - justify-content: center; - margin: auto; - white-space: pre-wrap; - word-break: break-word; + display: block; + font-family: Fira, FiraCode, monospace; + background: #333; + padding: 0.5rem 1rem; + max-width: 40rem; + border-radius: 10px; + justify-content: center; + margin: auto; + white-space: pre-wrap; + word-break: break-word; } -summary { - cursor: pointer; +#errorInfo summary { + cursor: pointer; } -summary > * { - display: inline; - white-space: pre-wrap; +#errorInfo summary>* { + display: inline; } @media screen and (max-width: 500px) { - details { - width: 50%; - } + #errorInfo { + width: 50%; + } } diff --git a/packages/backend/src/server/web/error.js b/packages/backend/src/server/web/error.js new file mode 100644 index 000000000000..4838dd6ef3a0 --- /dev/null +++ b/packages/backend/src/server/web/error.js @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +'use strict'; + +(() => { + document.addEventListener('DOMContentLoaded', () => { + const locale = JSON.parse(localStorage.getItem('locale') || '{}'); + + const messages = Object.assign({ + title: 'Failed to initialize Misskey', + serverError: 'If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.', + solution: 'The following actions may solve the problem.', + solution1: 'Update your os and browser', + solution2: 'Disable an adblocker', + solution3: 'Clear the browser cache', + solution4: '(Tor Browser) Set dom.webaudio.enabled to true', + otherOption: 'Other options', + otherOption1: 'Clear preferences and cache', + otherOption2: 'Start the simple client', + otherOption3: 'Start the repair tool', + }, locale?._bootErrors || {}); + const reload = locale?.reload || 'Reload'; + + const reloadEls = document.querySelectorAll('[data-i18n-reload]'); + for (const el of reloadEls) { + el.textContent = reload; + } + + const i18nEls = document.querySelectorAll('[data-i18n]'); + for (const el of i18nEls) { + const key = el.dataset.i18n; + if (key && messages[key]) { + el.textContent = messages[key]; + } + } + }); +})(); diff --git a/packages/backend/src/server/web/views/error.pug b/packages/backend/src/server/web/views/error.pug index 44ebf53cf714..6a78d1878c2e 100644 --- a/packages/backend/src/server/web/views/error.pug +++ b/packages/backend/src/server/web/views/error.pug @@ -2,15 +2,15 @@ doctype html // - - _____ _ _ - | |_|___ ___| |_ ___ _ _ + _____ _ _ + | |_|___ ___| |_ ___ _ _ | | | | |_ -|_ -| '_| -_| | | |_|_|_|_|___|___|_,_|___|_ | - |___| + |___| Thank you for using Misskey! If you are reading this message... how about joining the development? https://github.com/misskey-dev/misskey - + html @@ -27,39 +27,45 @@ html style include ../error.css + script + include ../error.js + body svg.icon-warning(xmlns="http://www.w3.org/2000/svg", viewBox="0 0 24 24", stroke-width="2", stroke="currentColor", fill="none", stroke-linecap="round", stroke-linejoin="round") path(stroke="none", d="M0 0h24v24H0z", fill="none") path(d="M12 9v2m0 4v.01") path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75") - h1 An error has occurred! + h1(data-i18n="title") Failed to initialize Misskey button.button-big(onclick="location.reload();") - span.button-label-big Refresh - - p.dont-worry Don't worry, it's (probably) not your fault. + span.button-label-big(data-i18n-reload) Reload - p If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID. + p(data-i18n="serverError") If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID. div#errors code. ERROR CODE: #{code} ERROR ID: #{id} - p You may also try the following options: + p + b(data-i18n="solution") The following actions may solve the problem. - p Update your os and browser. - p Disable an adblocker. + p(data-i18n="solution1") Update your os and browser + p(data-i18n="solution2") Disable an adblocker + p(data-i18n="solution3") Clear your browser cache + p(data-i18n="solution4") (Tor Browser) Set dom.webaudio.enabled to true - a(href="/flush") - button.button-small - span.button-label-small Clear preferences and cache - br - a(href="/cli") - button.button-small - span.button-label-small Start the simple client - br - a(href="/bios") - button.button-small - span.button-label-small Start the repair tool + details(style="color: #86b300;") + summary(data-i18n="otherOption") Other options + a(href="/flush") + button.button-small + span.button-label-small(data-i18n="otherOption1") Clear preferences and cache + br + a(href="/cli") + button.button-small + span.button-label-small(data-i18n="otherOption2") Start the simple client + br + a(href="/bios") + button.button-small + span.button-label-small(data-i18n="otherOption3") Start the repair tool diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index df3cfee17103..bf409031c88f 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -18,6 +18,7 @@ * achievementEarned - 実績を獲得 * exportCompleted - エクスポートが完了 * login - ログイン + * createToken - トークン作成 * app - アプリ通知 * test - テスト通知(サーバー側) */ @@ -36,6 +37,7 @@ export const notificationTypes = [ 'achievementEarned', 'exportCompleted', 'login', + 'createToken', 'app', 'test', ] as const; diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts index 220c22e198cd..1584f9587e8f 100644 --- a/packages/backend/test-federation/test/note.test.ts +++ b/packages/backend/test-federation/test/note.test.ts @@ -139,29 +139,99 @@ describe('Note', () => { }); describe('Deletion', () => { - describe('Check Delete consistency', () => { - let carol: LoginUser; - - beforeAll(async () => { - carol = await createAccount('a.test'); + describe('Check Delete is delivered', () => { + describe('To followers', () => { + let carol: LoginUser; + + beforeAll(async () => { + carol = await createAccount('a.test'); + + await carol.client.request('following/create', { userId: bobInA.id }); + await sleep(); + }); + + test('Check', async () => { + const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; + const noteInA = await resolveRemoteNote('b.test', note.id, carol); + await bob.client.request('notes/delete', { noteId: note.id }); + await sleep(); + + await rejects( + async () => await carol.client.request('notes/show', { noteId: noteInA.id }), + (err: any) => { + strictEqual(err.code, 'NO_SUCH_NOTE'); + return true; + }, + ); + }); + + afterAll(async () => { + await carol.client.request('following/delete', { userId: bobInA.id }); + await sleep(); + }); + }); - await carol.client.request('following/create', { userId: bobInA.id }); - await sleep(); + describe('To renoted and not followed user', () => { + test('Check', async () => { + const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; + const noteInA = await resolveRemoteNote('b.test', note.id, alice); + await alice.client.request('notes/create', { renoteId: noteInA.id }); + await sleep(); + + await bob.client.request('notes/delete', { noteId: note.id }); + await sleep(); + + await rejects( + async () => await alice.client.request('notes/show', { noteId: noteInA.id }), + (err: any) => { + strictEqual(err.code, 'NO_SUCH_NOTE'); + return true; + }, + ); + }); }); - test('Delete is derivered to followers', async () => { - const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; - const noteInA = await resolveRemoteNote('b.test', note.id, carol); - await bob.client.request('notes/delete', { noteId: note.id }); - await sleep(); + describe('To replied and not followed user', () => { + test('Check', async () => { + const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; + const noteInA = await resolveRemoteNote('b.test', note.id, alice); + await alice.client.request('notes/create', { text: 'Hello Bob!', replyId: noteInA.id }); + await sleep(); + + await bob.client.request('notes/delete', { noteId: note.id }); + await sleep(); + + await rejects( + async () => await alice.client.request('notes/show', { noteId: noteInA.id }), + (err: any) => { + strictEqual(err.code, 'NO_SUCH_NOTE'); + return true; + }, + ); + }); + }); - await rejects( - async () => await carol.client.request('notes/show', { noteId: noteInA.id }), - (err: any) => { - strictEqual(err.code, 'NO_SUCH_NOTE'); - return true; - }, - ); + /** + * FIXME: not delivered + * @see https://github.com/misskey-dev/misskey/issues/15548 + */ + describe('To only resolved and not followed user', () => { + test.failing('Check', async () => { + const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; + const noteInA = await resolveRemoteNote('b.test', note.id, alice); + await sleep(); + + await bob.client.request('notes/delete', { noteId: note.id }); + await sleep(); + + await rejects( + async () => await alice.client.request('notes/show', { noteId: noteInA.id }), + (err: any) => { + strictEqual(err.code, 'NO_SUCH_NOTE'); + return true; + }, + ); + }); }); }); diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts index 093277cdb498..db8da5025aff 100644 --- a/packages/backend/test-federation/test/utils.ts +++ b/packages/backend/test-federation/test/utils.ts @@ -22,7 +22,7 @@ export type LoginUser = SigninResponse & { client: Misskey.api.APIClient; username: string; password: string; -} +}; /** used for avoiding overload and some endpoints */ export type Request = < diff --git a/packages/backend/test-server/.swcrc b/packages/backend/test-server/.swcrc index e3d6935169ed..eeac7eabc60e 100644 --- a/packages/backend/test-server/.swcrc +++ b/packages/backend/test-server/.swcrc @@ -1,5 +1,5 @@ { - "$schema": "https://json.schemastore.org/swcrc", + "$schema": "https://swc.rs/schema.json", "jsc": { "parser": { "syntax": "typescript", diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index a130c3698db7..7ae1ee452326 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -182,7 +182,6 @@ describe('クリップ', () => { { label: 'nameがnull', parameters: { name: null } }, { label: 'nameが最大長+1', parameters: { name: 'x'.repeat(101) } }, { label: 'isPublicがboolじゃない', parameters: { isPublic: 'true' } }, - { label: 'descriptionがゼロ長', parameters: { description: '' } }, { label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } }, ]; test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({ @@ -199,6 +198,23 @@ describe('クリップ', () => { id: '3d81ceae-475f-4600-b2a8-2bc116157532', })); + test('の作成はdescriptionが空文字ならnullになる', async () => { + const clip = await successfulApiCall({ + endpoint: 'clips/create', + parameters: { + ...defaultCreate(), + description: '', + }, + user: alice, + }); + + assert.deepStrictEqual(clip, { + ...clip, + ...defaultCreate(), + description: null, + }); + }); + test('の更新ができる', async () => { const res = await update({ clipId: (await create()).id, @@ -249,6 +265,24 @@ describe('クリップ', () => { ...assertion, })); + test('の更新はdescriptionが空文字ならnullになる', async () => { + const clip = await successfulApiCall({ + endpoint: 'clips/update', + parameters: { + clipId: (await create()).id, + name: 'updated', + description: '', + }, + user: alice, + }); + + assert.deepStrictEqual(clip, { + ...clip, + name: 'updated', + description: null, + }); + }); + test('の削除ができる', async () => { await deleteClip({ clipId: (await create()).id, diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 8ea4cb9800cd..b85cebf72416 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -10,13 +10,13 @@ import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet import type { SimpleGetResponse } from '../utils.js'; import type * as misskey from 'misskey-js'; -// Request Accept +// Request Accept in lowercase const ONLY_AP = 'application/activity+json'; const PREFER_AP = 'application/activity+json, */*'; const PREFER_HTML = 'text/html, */*'; const UNSPECIFIED = '*/*'; -// Response Content-Type +// Response Content-Type in lowercase const AP = 'application/activity+json; charset=utf-8'; const HTML = 'text/html; charset=utf-8'; const JSON_UTF8 = 'application/json; charset=utf-8'; @@ -44,7 +44,8 @@ describe('Webリソース', () => { const { path, accept, cookie, type } = param; const res = await simpleGet(path, accept, cookie); assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, type ?? HTML); + // Header values are case-insensitive + assert.strictEqual(res.type?.toLowerCase(), (type ?? HTML).toLowerCase()); return res; }; @@ -95,8 +96,7 @@ describe('Webリソース', () => { describe.each([ { path: '/', type: HTML }, { path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。" - // fastify-static gives charset=UTF-8 instead of utf-8 and that's okay - { path: '/api-doc', type: 'text/html; charset=UTF-8' }, + { path: '/api-doc', type: HTML }, { path: '/api.json', type: JSON_UTF8 }, { path: '/api-console', type: HTML }, { path: '/_info_card_', type: HTML }, diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 319c8581f40b..d6d2cb33f0bf 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -397,7 +397,7 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); - }, 1000 * 15); + }, 1000 * 30); test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 36af8823f6fc..7350da3caece 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -24,13 +24,13 @@ describe('MfmService', () => { describe('toHtml', () => { test('br', () => { const input = 'foo\nbar\nbaz'; - const output = '

foo
bar
baz

'; + const output = '

foo
bar
baz

'; assert.equal(mfmService.toHtml(mfm.parse(input)), output); }); test('br alt', () => { const input = 'foo\r\nbar\rbaz'; - const output = '

foo
bar
baz

'; + const output = '

foo
bar
baz

'; assert.equal(mfmService.toHtml(mfm.parse(input)), output); }); diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts index 9676abf07b74..3b3d212c306e 100644 --- a/packages/backend/test/unit/RelayService.ts +++ b/packages/backend/test/unit/RelayService.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { UtilityService } from '@/core/UtilityService.js'; + process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; @@ -40,6 +42,7 @@ describe('RelayService', () => { ApRendererService, RelayService, UserEntityService, + UtilityService, ], }) .useMocker((token) => { diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index d3d39240dc9a..0426de8e1981 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -8,6 +8,8 @@ import httpSignature from '@peertube/http-signature'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; +import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; +import { IObject } from '@/core/activitypub/type.js'; export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { return { @@ -24,6 +26,10 @@ export const buildParsedSignature = (signingString: string, signature: string, a }; }; +function cartesianProduct(a: T[], b: U[]): [T, U][] { + return a.flatMap(a => b.map(b => [a, b] as [T, U])); +} + describe('ap-request', () => { test('createSignedPost with verify', async () => { const keypair = await genRsaKeyPair(); @@ -58,4 +64,123 @@ describe('ap-request', () => { const result = httpSignature.verifySignature(parsed, keypair.publicKey); assert.deepStrictEqual(result, true); }); + + test('rejects non matching domain', () => { + assert.doesNotThrow(() => assertActivityMatchesUrls( + 'https://alice.example.com/abc', + { id: 'https://alice.example.com/abc' } as IObject, + [ + 'https://alice.example.com/abc', + ], + FetchAllowSoftFailMask.Strict, + ), 'validation should pass base case'); + assert.throws(() => assertActivityMatchesUrls( + 'https://alice.example.com/abc', + { id: 'https://bob.example.com/abc' } as IObject, + [ + 'https://alice.example.com/abc', + ], + FetchAllowSoftFailMask.Any, + ), 'validation should fail no matter what if the response URL is inconsistent with the object ID'); + + // fix issues like threads + // https://github.com/misskey-dev/misskey/issues/15039 + const withOrWithoutWWW = [ + 'https://alice.example.com/abc', + 'https://www.alice.example.com/abc', + ]; + + cartesianProduct( + cartesianProduct( + withOrWithoutWWW, + withOrWithoutWWW, + ), + withOrWithoutWWW, + ).forEach(([[a, b], c]) => { + assert.doesNotThrow(() => assertActivityMatchesUrls( + a, + { id: b } as IObject, + [ + c, + ], + FetchAllowSoftFailMask.Strict, + ), 'validation should pass with or without www. subdomain'); + }); + }); + + test('cross origin lookup', () => { + assert.doesNotThrow(() => assertActivityMatchesUrls( + 'https://alice.example.com/abc', + { id: 'https://bob.example.com/abc' } as IObject, + [ + 'https://bob.example.com/abc', + ], + FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId, + ), 'validation should pass if the response is otherwise consistent and cross-origin is allowed'); + assert.throws(() => assertActivityMatchesUrls( + 'https://alice.example.com/abc', + { id: 'https://bob.example.com/abc' } as IObject, + [ + 'https://bob.example.com/abc', + ], + FetchAllowSoftFailMask.Strict, + ), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed'); + }); + + test('rejects non-canonical ID', () => { + assert.throws(() => assertActivityMatchesUrls( + 'https://alice.example.com/@alice', + { id: 'https://alice.example.com/users/alice' } as IObject, + [ + 'https://alice.example.com/users/alice' + ], + FetchAllowSoftFailMask.Strict, + ), 'throws if the response ID did not exactly match the expected ID'); + assert.doesNotThrow(() => assertActivityMatchesUrls( + 'https://alice.example.com/@alice', + { id: 'https://alice.example.com/users/alice' } as IObject, + [ + 'https://alice.example.com/users/alice', + ], + FetchAllowSoftFailMask.NonCanonicalId, + ), 'does not throw if non-canonical ID is allowed'); + }); + + test('origin relaxed alignment', () => { + assert.doesNotThrow(() => assertActivityMatchesUrls( + 'https://alice.example.com/abc', + { id: 'https://ap.alice.example.com/abc' } as IObject, + [ + 'https://ap.alice.example.com/abc', + ], + FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId, + ), 'validation should pass if response is a subdomain of the expected origin'); + assert.throws(() => assertActivityMatchesUrls( + 'https://alice.multi-tenant.example.com/abc', + { id: 'https://alice.multi-tenant.example.com/abc' } as IObject, + [ + 'https://bob.multi-tenant.example.com/abc', + ], + FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId, + ), 'validation should fail if response is a disjoint domain of the expected origin'); + assert.throws(() => assertActivityMatchesUrls( + 'https://alice.example.com/abc', + { id: 'https://ap.alice.example.com/abc' } as IObject, + [ + 'https://ap.alice.example.com/abc', + ], + FetchAllowSoftFailMask.Strict, + ), 'throws if relaxed origin is forbidden'); + }); + + test('resist HTTP downgrade', () => { + assert.throws(() => assertActivityMatchesUrls( + 'https://alice.example.com/abc', + { id: 'https://alice.example.com/abc' } as IObject, + [ + 'http://alice.example.com/abc', + ], + FetchAllowSoftFailMask.Strict, + ), 'throws if HTTP downgrade is detected'); + }); }); diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js index dd8f03dac510..7805256fd4de 100644 --- a/packages/frontend-embed/eslint.config.js +++ b/packages/frontend-embed/eslint.config.js @@ -47,6 +47,7 @@ export default [ '@typescript-eslint/no-empty-interface': ['error', { allowSingleExtends: true, }], + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため 'id-denylist': ['error', 'window', 'e'], diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index ab5026ab0d1b..476835686543 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -12,12 +12,12 @@ "dependencies": { "@discordapp/twemoji": "15.1.0", "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-replace": "5.0.7", - "@rollup/pluginutils": "5.1.3", - "@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz", + "@rollup/plugin-replace": "6.0.2", + "@rollup/pluginutils": "5.1.4", + "@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz", "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.2.0", - "@vue/compiler-sfc": "3.5.12", + "@vitejs/plugin-vue": "5.2.1", + "@vue/compiler-sfc": "3.5.13", "astring": "1.9.0", "buraha": "0.0.1", "estree-walker": "3.0.3", @@ -25,47 +25,46 @@ "misskey-js": "workspace:*", "frontend-shared": "workspace:*", "punycode.js": "2.3.1", - "rollup": "4.26.0", - "sass": "1.79.4", - "shiki": "1.22.2", + "rollup": "4.34.8", + "sass": "1.85.0", + "shiki": "3.0.0", "tinycolor2": "1.6.0", "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", - "typescript": "5.6.3", - "uuid": "10.0.0", + "typescript": "5.7.3", + "uuid": "11.1.0", "json5": "2.2.3", - "vite": "5.4.11", - "vue": "3.5.12" + "vite": "6.1.1", + "vue": "3.5.13" }, "devDependencies": { - "@misskey-dev/summaly": "5.1.0", + "@misskey-dev/summaly": "5.2.0", "@testing-library/vue": "8.1.0", "@types/estree": "1.0.6", "@types/micromatch": "4.0.9", - "@types/node": "22.9.0", + "@types/node": "22.13.5", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", - "@types/uuid": "10.0.0", - "@types/ws": "8.5.13", - "@typescript-eslint/eslint-plugin": "7.17.0", - "@typescript-eslint/parser": "7.17.0", - "@vitest/coverage-v8": "1.6.0", - "@vue/runtime-core": "3.5.12", + "@types/ws": "8.5.14", + "@typescript-eslint/eslint-plugin": "8.24.1", + "@typescript-eslint/parser": "8.24.1", + "@vitest/coverage-v8": "3.0.6", + "@vue/runtime-core": "3.5.13", "acorn": "8.14.0", "cross-env": "7.0.3", "eslint-plugin-import": "2.31.0", - "eslint-plugin-vue": "9.31.0", - "fast-glob": "3.3.2", - "happy-dom": "10.0.3", + "eslint-plugin-vue": "9.32.0", + "fast-glob": "3.3.3", + "happy-dom": "17.1.4", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "msw": "2.6.4", - "nodemon": "3.1.7", - "prettier": "3.3.3", - "start-server-and-test": "2.0.8", + "msw": "2.7.1", + "nodemon": "3.1.9", + "prettier": "3.5.2", + "start-server-and-test": "2.0.10", "vite-plugin-turbosnap": "1.0.3", - "vue-component-type-helpers": "2.1.10", + "vue-component-type-helpers": "2.2.4", "vue-eslint-parser": "9.4.3", - "vue-tsc": "2.1.10" + "vue-tsc": "2.2.4" } } diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts index e84fd6f679e0..1f9ce9d4f436 100644 --- a/packages/frontend-embed/src/components/EmMfm.ts +++ b/packages/frontend-embed/src/components/EmMfm.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { VNode, h, SetupContext, provide } from 'vue'; +import { h, provide } from 'vue'; +import type { VNode, SetupContext } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import { host } from '@@/js/config.js'; diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue index 4e0ae005dfff..962a982fb7aa 100644 --- a/packages/frontend-embed/src/components/EmNotes.vue +++ b/packages/frontend-embed/src/components/EmNotes.vue @@ -22,7 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only + diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 08817fd6a8fa..739061bce1c5 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -44,12 +44,14 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts b/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts index 411d62edf9c8..b090f0a0faeb 100644 --- a/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts +++ b/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts @@ -4,7 +4,7 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { http, HttpResponse } from 'msw'; import { role } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index eeadd4993698..15717802ecf4 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -40,11 +40,29 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue index cc8b625cd5e7..d6ee8ea49cef 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue @@ -82,7 +82,6 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { emptyStrToEmptyArray, emptyStrToNull, - RequestLogItem, roleIdsParser, } from '@/pages/admin/custom-emojis-manager.impl.js'; import MkGrid from '@/components/grid/MkGrid.vue'; @@ -96,12 +95,15 @@ import * as os from '@/os.js'; import { validators } from '@/components/grid/cell-validators.js'; import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js'; import { uploadFile } from '@/scripts/upload.js'; -import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; -import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js'; +import { extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js'; import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; -import { GridSetting } from '@/components/grid/grid.js'; import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; -import { GridRow } from '@/components/grid/row.js'; + +import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; +import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; +import type { DroppedFile } from '@/scripts/file-drop.js'; +import type { GridSetting } from '@/components/grid/grid.js'; +import type { GridRow } from '@/components/grid/row.js'; const MAXIMUM_EMOJI_REGISTER_COUNT = 100; @@ -122,7 +124,7 @@ type GridItem = { localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[]; type: string | null; -} +}; function setupGrid(): GridSetting { const $style = useCssModule(); diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue index eecf8d739099..609d445d7914 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue @@ -148,30 +148,27 @@ import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkGrid from '@/components/grid/MkGrid.vue'; -import { - emptyStrToUndefined, - GridSortOrderKey, - gridSortOrderKeys, - RequestLogItem, -} from '@/pages/admin/custom-emojis-manager.impl.js'; -import { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; +import { emptyStrToUndefined, gridSortOrderKeys } from '@/pages/admin/custom-emojis-manager.impl.js'; import MkFolder from '@/components/MkFolder.vue'; import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; import * as os from '@/os.js'; -import { GridSetting } from '@/components/grid/grid.js'; import { deviceKind } from '@/scripts/device-kind.js'; import MkPagingButtons from '@/components/MkPagingButtons.vue'; import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue'; -import { SortOrder } from '@/components/MkSortOrderEditor.define.js'; import { useLoading } from '@/components/hook/useLoading.js'; +import type { GridSortOrderKey, RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; +import type { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; +import type { GridSetting } from '@/components/grid/grid.js'; +import type { SortOrder } from '@/components/MkSortOrderEditor.define.js'; + type GridItem = { checked: boolean; id: string; url: string; name: string; host: string; -} +}; function setupGrid(): GridSetting { const $style = useCssModule(); diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts b/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts index f62304277a84..3384a71f0f2c 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts +++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts @@ -4,7 +4,7 @@ */ import { delay, http, HttpResponse } from 'msw'; -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { entities } from 'misskey-js'; import { commonHandlers } from '../../../.storybook/mocks.js'; import { emoji } from '../../../.storybook/fakes.js'; diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index 91f41166e990..a312ecce1236 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -8,20 +8,34 @@ SPDX-License-Identifier: AGPL-3.0-only - - +
+ + -
- - - - - - - - Save -
-
+
+ + + + + Save +
+ + + + + +
+ + + + + + + + Save +
+
+
@@ -44,10 +58,13 @@ import MkFolder from '@/components/MkFolder.vue'; const deeplAuthKey = ref(''); const deeplIsPro = ref(false); +const googleAnalyticsMeasurementId = ref(''); + async function init() { const meta = await misskeyApi('admin/meta'); - deeplAuthKey.value = meta.deeplAuthKey; + deeplAuthKey.value = meta.deeplAuthKey ?? ''; deeplIsPro.value = meta.deeplIsPro; + googleAnalyticsMeasurementId.value = meta.googleAnalyticsMeasurementId ?? ''; } function save_deepl() { @@ -59,6 +76,14 @@ function save_deepl() { }); } +function save_googleAnalytics() { + os.apiWithDialog('admin/update-meta', { + googleAnalyticsMeasurementId: googleAnalyticsMeasurementId.value, + }).then(() => { + fetchInstance(true); + }); +} + const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index ea5fa457f230..672ee8da18e2 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -41,7 +41,8 @@ import { lookup } from '@/scripts/lookup.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js'; -import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; +import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; +import type { PageMetadata } from '@/scripts/page-metadata.js'; import { useRouter } from '@/router/supplier.js'; const isEmpty = (x: string | null) => x == null || x === ''; diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue index 9cb430b0fe10..5189e1289999 100644 --- a/packages/frontend/src/pages/admin/invites.vue +++ b/packages/frontend/src/pages/admin/invites.vue @@ -65,7 +65,8 @@ import MkFolder from '@/components/MkFolder.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import MkPagination from '@/components/MkPagination.vue'; +import type { Paging } from '@/components/MkPagination.vue'; import MkInviteCode from '@/components/MkInviteCode.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts b/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts index 584cd3e4d954..88747ef4ed32 100644 --- a/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts +++ b/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { http, HttpResponse } from 'msw'; import { action } from '@storybook/addon-actions'; import { commonHandlers } from '../../../.storybook/mocks.js'; diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue index 0896859f3cf0..7d80f8c2e3fe 100644 --- a/packages/frontend/src/pages/admin/overview.federation.vue +++ b/packages/frontend/src/pages/admin/overview.federation.vue @@ -47,7 +47,8 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/search.stories.impl.ts b/packages/frontend/src/pages/search.stories.impl.ts index 0110a7ab8ed1..27271615c292 100644 --- a/packages/frontend/src/pages/search.stories.impl.ts +++ b/packages/frontend/src/pages/search.stories.impl.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { StoryObj } from '@storybook/vue3'; +import type { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import search_ from './search.vue'; import { userDetailed } from '@/../.storybook/fakes.js'; diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index e8bc4cd6d3ac..2b8faf5465e0 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -49,14 +49,16 @@ const props = withDefaults(defineProps<{ const router = useRouter(); -const key = ref(''); +const key = ref(0); +const userPagination = ref>(); + const searchQuery = ref(toRef(props, 'query').value); const searchOrigin = ref(toRef(props, 'origin').value); -const userPagination = ref(); async function search() { const query = searchQuery.value.toString().trim(); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (query == null || query === '') return; //#region AP lookup @@ -76,6 +78,7 @@ async function search() { if (res.type === 'User') { router.push(`/@${res.object.username}@${res.object.host}`); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (res.type === 'Note') { router.push(`/notes/${res.object.id}`); } @@ -118,6 +121,6 @@ async function search() { }, }; - key.value = query; + key.value++; } diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index c2588736b35a..4a7301f40520 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only