Compare commits

..

1 Commits

Author SHA1 Message Date
Kisaragi
fcb9f8bef4 docs: FEP-67ff 2025-02-08 13:35:10 +09:00
173 changed files with 7076 additions and 8004 deletions

View File

@@ -220,10 +220,5 @@ allowedPrivateNetworks: [
'127.0.0.1/32' '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) # Upload or download file size limits (bytes)
#maxFileSize: 262144000 #maxFileSize: 262144000

View File

@@ -235,11 +235,6 @@ signToActivityPubGet: true
# '127.0.0.1/32' # '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) # Upload or download file size limits (bytes)
#maxFileSize: 262144000 #maxFileSize: 262144000

View File

@@ -334,11 +334,6 @@ signToActivityPubGet: true
# '127.0.0.1/32' # '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) # Upload or download file size limits (bytes)
#maxFileSize: 262144000 #maxFileSize: 262144000

View File

@@ -7,9 +7,7 @@
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "22.11.0" "version": "22.11.0"
}, },
"ghcr.io/devcontainers-extra/features/corepack:1": { "ghcr.io/devcontainers-contrib/features/corepack:1": {}
"version": "0.31.0"
}
}, },
"forwardPorts": [3000], "forwardPorts": [3000],
"postCreateCommand": "/bin/bash .devcontainer/init.sh", "postCreateCommand": "/bin/bash .devcontainer/init.sh",

View File

@@ -9,7 +9,7 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: daily interval: daily
open-pull-requests-limit: 0 open-pull-requests-limit: 100
# Add only the root, not each workspace item # Add only the root, not each workspace item
# https://github.com/dependabot/dependabot-core/issues/4993#issuecomment-1289133027 # https://github.com/dependabot/dependabot-core/issues/4993#issuecomment-1289133027
@@ -17,7 +17,7 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: daily interval: daily
open-pull-requests-limit: 0 open-pull-requests-limit: 10
# List dependencies required to be updated together, sharing the same version numbers. # 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. # Those who simply have the common owner (e.g. @fastify) don't need to be listed.
groups: groups:

View File

@@ -20,12 +20,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -12,9 +12,9 @@ jobs:
steps: steps:
- name: Checkout head - name: Checkout head
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'

View File

@@ -18,7 +18,7 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
persist-credentials: false persist-credentials: false
@@ -29,7 +29,7 @@ jobs:
- name: setup node - name: setup node
id: setup-node id: setup-node
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: pnpm cache: pnpm
@@ -66,7 +66,7 @@ jobs:
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
persist-credentials: false persist-credentials: false

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
- name: Check version - name: Check version
run: | run: |
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
- name: Check - name: Check
run: | run: |
counter=0 counter=0

View File

@@ -10,7 +10,7 @@ jobs:
check_copyright_year: check_copyright_year:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
- run: | - run: |
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
echo "Please change copyright year!" echo "Please change copyright year!"

View File

@@ -28,7 +28,7 @@ jobs:
wait_time: ${{ steps.get-wait-time.outputs.wait_time }} wait_time: ${{ steps.get-wait-time.outputs.wait_time }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
- name: Check allowed users - name: Check allowed users
id: check-allowed-users id: check-allowed-users

View File

@@ -27,7 +27,7 @@ jobs:
platform=${{ matrix.platform }} platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub - name: Log in to Docker Hub

View File

@@ -32,7 +32,7 @@ jobs:
platform=${{ matrix.platform }} platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Docker meta - name: Docker meta

View File

@@ -15,7 +15,7 @@ jobs:
DOCKER_CONTENT_TRUST: 1 DOCKER_CONTENT_TRUST: 1
DOCKLE_VERSION: 0.4.14 DOCKLE_VERSION: 0.4.14
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
- name: Download and install dockle v${{ env.DOCKLE_VERSION }} - name: Download and install dockle v${{ env.DOCKLE_VERSION }}
run: | run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb" curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"

View File

@@ -30,14 +30,14 @@ jobs:
ref: refs/pull/${{ github.event.number }}/merge ref: refs/pull/${{ github.event.number }}/merge
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
ref: ${{ matrix.ref }} ref: ${{ matrix.ref }}
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -36,12 +36,12 @@ jobs:
pnpm_install: pnpm_install:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.2.0 - uses: actions/setup-node@v4.1.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@@ -67,19 +67,19 @@ jobs:
eslint-cache-version: v1 eslint-cache-version: v1
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }} eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.2.0 - uses: actions/setup-node@v4.1.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
- run: corepack enable - run: corepack enable
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- name: Restore eslint cache - name: Restore eslint cache
uses: actions/cache@v4.2.1 uses: actions/cache@v4.2.0
with: with:
path: ${{ env.eslint-cache-path }} path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
@@ -97,12 +97,12 @@ jobs:
- sw - sw
- misskey-js - misskey-js
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.2.0 - uses: actions/setup-node@v4.1.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -18,12 +18,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4.2.0 - uses: actions/setup-node@v4.1.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

36
.github/workflows/ok-to-test.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
# 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

View File

@@ -23,13 +23,13 @@ jobs:
node-version: [22.11.0] node-version: [22.11.0]
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

92
.github/workflows/pr-preview-deploy.yml vendored Normal file
View File

@@ -0,0 +1,92 @@
# 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'
# <insert integration tests needing secrets>
- 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;

View File

@@ -0,0 +1,54 @@
# 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

View File

@@ -26,12 +26,12 @@ jobs:
NODE_OPTIONS: "--max_old_space_size=7168" NODE_OPTIONS: "--max_old_space_size=7168"
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
if: github.event_name != 'pull_request_target' if: github.event_name != 'pull_request_target'
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
if: github.event_name == 'pull_request_target' if: github.event_name == 'pull_request_target'
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -46,7 +46,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js 20.x - name: Use Node.js 20.x
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -45,7 +45,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
@@ -66,7 +66,7 @@ jobs:
fi fi
done done
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@@ -108,13 +108,13 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -47,7 +47,7 @@ jobs:
fi fi
done done
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -36,13 +36,13 @@ jobs:
node-version: [22.11.0] node-version: [22.11.0]
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@@ -86,7 +86,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150 # https://github.com/cypress-io/cypress-docker-images/issues/150
@@ -98,7 +98,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -31,12 +31,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -21,13 +21,13 @@ jobs:
node-version: [22.11.0] node-version: [22.11.0]
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -25,13 +25,13 @@ jobs:
node-version: [22.11.0] node-version: [22.11.0]
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.2.0 uses: actions/setup-node@v4.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@@ -1,33 +1,15 @@
## 2025.2.1 ## Unreleased
### General ### General
- Feat: アクセストークン発行時に通知するように -
- Feat: 実験的なGoogleAnalyticsサポートを追加
- 依存関係の更新
### Client ### Client
- Feat: 投稿フォームで画像をプレビュー可能に
- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように - Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように
- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992` - Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
- Enhance: クライアントエラー画面の多言語対応
- Enhance: 開発者モードでメニューからファイルIDをコピー出来るように `#15441'
- Enhance: ノートに埋め込まれたメディアのコンテキストメニューから管理者用のファイル管理画面を開けるように ( #15440 )
- Enhance: リアクションする際に確認ダイアログを表示できるように
- Enhance: CWの注釈で入力済みの文字数を表示
- Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529`
- Fix: 埋め込みプレイヤーから外部ページに移動できない問題を修正
- Fix: Play の再読込時に UI が以前の状態を引き継いでしまう問題を修正 `#14378`
- Fix: カスタム絵文字管理画面(beta)にてisSensitive/localOnlyの絞り込みが上手くいかない問題の修正 ( #15445 )
- Fix: CWの注釈が100文字を超えている場合、ート投稿ボタンを非アクティブに
### Server ### Server
- Enhance: 成り済まし対策として、ActivityPub照会された時にリモートのリダイレクトを拒否できるように (config.disallowExternalApRedirect) -
- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように
- Fix: オブジェクトストレージの設定でPrefixを設定していなかった場合nullまたは空文字になる問題を修正
- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886)
- Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように
- Fix: フォロワーではないユーザーにリートもしくは返信された場合にートのDeleteアクティビティが送られていない問題を修正
## 2025.2.0 ## 2025.2.0

20
FEDERATION.md Normal file
View File

@@ -0,0 +1,20 @@
# Federation
## Supported federation protocols and standards
- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server)
- [WebFinger](https://webfinger.net/)
- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
- [NodeInfo](https://nodeinfo.diaspora.software/)
## Supported FEPs
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
## ActivityPub
This section is documented on [misskey-hub.net](https://misskey-hub.net/ns).
## Additional documentation
* https://misskey-hub.net/ns

70
locales/index.d.ts vendored
View File

@@ -5254,14 +5254,6 @@ export interface Locale extends ILocale {
* このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。 * このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。
*/ */
"federationDisabled": string; "federationDisabled": string;
/**
* リアクションする際に確認する
*/
"confirmOnReact": string;
/**
* " {emoji} " をリアクションしますか?
*/
"reactAreYouSure": ParameterizedString<"emoji">;
"_accountSettings": { "_accountSettings": {
/** /**
* コンテンツの表示にログインを必須にする * コンテンツの表示にログインを必須にする
@@ -9480,14 +9472,6 @@ export interface Locale extends ILocale {
* ログインがありました * ログインがありました
*/ */
"login": string; "login": string;
/**
* アクセストークンが作成されました
*/
"createToken": string;
/**
* 心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。
*/
"createTokenDescription": ParameterizedString<"text">;
"_types": { "_types": {
/** /**
* すべて * すべて
@@ -10896,7 +10880,13 @@ export interface Locale extends ILocale {
*/ */
"title": string; "title": string;
/** /**
* このサーバーと通信することはできましたが、得られたデータが不正なものでした。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。 * このサーバーと通信することはできましたが、得られたデータが不正なものでした。
*/
"description": string;
};
"_responseInvalidIdHostNotMatch": {
/**
* 入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。
*/ */
"description": string; "description": string;
}; };
@@ -10954,52 +10944,6 @@ 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;
};
} }
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;

View File

@@ -1309,8 +1309,6 @@ availableRoles: "利用可能なロール"
acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。" acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
confirmOnReact: "リアクションする際に確認する"
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
_accountSettings: _accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする" requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
@@ -2502,8 +2500,6 @@ _notification:
flushNotification: "通知の履歴をリセットする" flushNotification: "通知の履歴をリセットする"
exportOfXCompleted: "{x}のエクスポートが完了しました" exportOfXCompleted: "{x}のエクスポートが完了しました"
login: "ログインがありました" login: "ログインがありました"
createToken: "アクセストークンが作成されました"
createTokenDescription: "心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。"
_types: _types:
all: "すべて" all: "すべて"
@@ -2911,7 +2907,9 @@ _remoteLookupErrors:
description: "このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。" description: "このサーバーとの通信に失敗しました。相手サーバーがダウンしている可能性があります。また、不正なURIや存在しないURIを入力していないか確認してください。"
_responseInvalid: _responseInvalid:
title: "レスポンスが不正です" title: "レスポンスが不正です"
description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。" description: "このサーバーと通信することはできましたが、得られたデータが不正なものでした。"
_responseInvalidIdHostNotMatch:
description: "入力されたURIのドメインと最終的に得られたURIのドメインとが異なります。第三者のサーバーを介してリモートのコンテンツを照会している場合は、発信元のサーバーで取得できるURIを使用して照会し直してください。"
_noSuchObject: _noSuchObject:
title: "見つかりません" title: "見つかりません"
description: "要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。" description: "要求されたリソースは見つかりませんでした。URIをもう一度お確かめください。"
@@ -2929,16 +2927,3 @@ _captcha:
_unknown: _unknown:
title: "CAPTCHAエラー" title: "CAPTCHAエラー"
text: "想定外のエラーが発生しました。" text: "想定外のエラーが発生しました。"
_bootErrors:
title: "読み込みに失敗しました"
serverError: "少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。"
solution: "以下を行うと解決する可能性があります。"
solution1: "ブラウザおよびOSを最新バージョンに更新する"
solution2: "アドブロッカーを無効にする"
solution3: "ブラウザのキャッシュをクリアする"
solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する"
otherOption: "その他のオプション"
otherOption1: "クライアント設定とキャッシュを削除"
otherOption2: "簡易クライアントを起動"
otherOption3: "修復ツールを起動"

View File

@@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.2.1-beta.1", "version": "2025.2.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@9.15.4", "packageManager": "pnpm@9.6.0",
"workspaces": [ "workspaces": [
"packages/frontend-shared", "packages/frontend-shared",
"packages/frontend", "packages/frontend",
@@ -47,35 +47,35 @@
"cleanall": "pnpm clean-all" "cleanall": "pnpm clean-all"
}, },
"resolutions": { "resolutions": {
"chokidar": "3.6.0", "chokidar": "3.5.3",
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"dependencies": { "dependencies": {
"cssnano": "7.0.6", "cssnano": "6.1.2",
"execa": "8.0.1", "execa": "8.0.1",
"fast-glob": "3.3.3", "fast-glob": "3.3.2",
"ignore-walk": "6.0.5", "ignore-walk": "6.0.5",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.5.2", "postcss": "8.4.49",
"tar": "6.2.1", "tar": "6.2.1",
"terser": "5.39.0", "terser": "5.36.0",
"typescript": "5.7.3", "typescript": "5.6.3",
"esbuild": "0.25.0", "esbuild": "0.24.0",
"glob": "11.0.1" "glob": "11.0.0"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0", "@misskey-dev/eslint-plugin": "2.0.3",
"@types/node": "22.13.4", "@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "8.24.0", "@typescript-eslint/parser": "7.17.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "14.0.3", "cypress": "13.15.2",
"eslint": "9.20.1", "eslint": "9.14.0",
"globals": "15.15.0", "globals": "15.12.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"start-server-and-test": "2.0.10" "start-server-and-test": "2.0.8"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "4.22.0" "@tensorflow/tfjs-core": "4.4.0"
} }
} }

View File

@@ -1,16 +0,0 @@
/*
* 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"`);
}
}

View File

@@ -37,20 +37,20 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.10.16", "@swc/core-darwin-arm64": "1.3.56",
"@swc/core-darwin-x64": "1.10.16", "@swc/core-darwin-x64": "1.3.56",
"@swc/core-freebsd-x64": "1.3.11", "@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.10.16", "@swc/core-linux-arm-gnueabihf": "1.3.56",
"@swc/core-linux-arm64-gnu": "1.10.16", "@swc/core-linux-arm64-gnu": "1.3.56",
"@swc/core-linux-arm64-musl": "1.10.16", "@swc/core-linux-arm64-musl": "1.3.56",
"@swc/core-linux-x64-gnu": "1.10.16", "@swc/core-linux-x64-gnu": "1.3.56",
"@swc/core-linux-x64-musl": "1.10.16", "@swc/core-linux-x64-musl": "1.3.56",
"@swc/core-win32-arm64-msvc": "1.10.16", "@swc/core-win32-arm64-msvc": "1.3.56",
"@swc/core-win32-ia32-msvc": "1.10.16", "@swc/core-win32-ia32-msvc": "1.3.56",
"@swc/core-win32-x64-msvc": "1.10.16", "@swc/core-win32-x64-msvc": "1.3.56",
"@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs": "4.4.0",
"@tensorflow/tfjs-node": "4.22.0", "@tensorflow/tfjs-node": "4.4.0",
"bufferutil": "4.0.9", "bufferutil": "4.0.7",
"slacc-android-arm-eabi": "0.0.10", "slacc-android-arm-eabi": "0.0.10",
"slacc-android-arm64": "0.0.10", "slacc-android-arm64": "0.0.10",
"slacc-darwin-arm64": "0.0.10", "slacc-darwin-arm64": "0.0.10",
@@ -64,37 +64,37 @@
"slacc-linux-x64-musl": "0.0.10", "slacc-linux-x64-musl": "0.0.10",
"slacc-win32-arm64-msvc": "0.0.10", "slacc-win32-arm64-msvc": "0.0.10",
"slacc-win32-x64-msvc": "0.0.10", "slacc-win32-x64-msvc": "0.0.10",
"utf-8-validate": "6.0.5" "utf-8-validate": "6.0.3"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.749.0", "@aws-sdk/client-s3": "3.620.0",
"@aws-sdk/lib-storage": "3.749.0", "@aws-sdk/lib-storage": "3.620.0",
"@bull-board/api": "6.7.7", "@bull-board/api": "6.5.0",
"@bull-board/fastify": "6.7.7", "@bull-board/fastify": "6.5.0",
"@bull-board/ui": "6.7.7", "@bull-board/ui": "6.5.0",
"@discordapp/twemoji": "15.1.0", "@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "5.0.2", "@fastify/accepts": "5.0.1",
"@fastify/cookie": "11.0.2", "@fastify/cookie": "11.0.1",
"@fastify/cors": "10.0.2", "@fastify/cors": "10.0.1",
"@fastify/express": "4.0.2", "@fastify/express": "4.0.1",
"@fastify/http-proxy": "10.0.2", "@fastify/http-proxy": "10.0.1",
"@fastify/multipart": "9.0.3", "@fastify/multipart": "9.0.1",
"@fastify/static": "8.1.0", "@fastify/static": "8.0.2",
"@fastify/view": "10.0.2", "@fastify/view": "10.0.1",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.0", "@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "0.1.67", "@napi-rs/canvas": "0.1.56",
"@nestjs/common": "11.0.9", "@nestjs/common": "10.4.7",
"@nestjs/core": "11.0.9", "@nestjs/core": "10.4.7",
"@nestjs/testing": "11.0.9", "@nestjs/testing": "10.4.7",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sentry/node": "8.55.0", "@sentry/node": "8.38.0",
"@sentry/profiling-node": "8.55.0", "@sentry/profiling-node": "8.38.0",
"@simplewebauthn/server": "12.0.0", "@simplewebauthn/server": "10.0.1",
"@sinonjs/fake-timers": "11.3.1", "@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0", "@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.6.0", "@swc/cli": "0.3.12",
"@swc/core": "1.10.16", "@swc/core": "1.9.2",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.17.1", "ajv": "8.17.1",
@@ -103,10 +103,10 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"bullmq": "5.41.1", "bullmq": "5.26.1",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.4.1", "chalk": "5.3.0",
"chalk-template": "1.1.0", "chalk-template": "1.1.0",
"chokidar": "3.6.0", "chokidar": "3.6.0",
"cli-highlight": "2.1.11", "cli-highlight": "2.1.11",
@@ -114,46 +114,46 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "5.2.1", "fastify": "5.0.0",
"fastify-raw-body": "5.0.0", "fastify-raw-body": "5.0.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.6.0", "file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.2", "form-data": "4.0.1",
"got": "14.4.6", "got": "14.4.4",
"happy-dom": "16.8.1", "happy-dom": "15.11.4",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"ioredis": "5.5.0", "ioredis": "5.4.1",
"ip-cidr": "4.0.2", "ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-svg": "5.1.0", "is-svg": "5.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "26.0.0", "jsdom": "24.1.1",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.3", "jsonld": "8.3.2",
"jsrsasign": "11.1.0", "jsrsasign": "11.1.0",
"juice": "11.0.0", "juice": "11.0.0",
"meilisearch": "0.48.2", "meilisearch": "0.45.0",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"microformats-parser": "2.0.2", "microformats-parser": "2.0.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nanoid": "5.1.0", "nanoid": "5.0.8",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.10.0", "nodemailer": "6.9.16",
"nsfwjs": "4.2.0", "nsfwjs": "4.2.0",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.3.6", "otpauth": "9.3.4",
"parse5": "7.2.1", "parse5": "7.2.1",
"pg": "8.13.3", "pg": "8.13.1",
"pkce-challenge": "4.1.0", "pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
@@ -167,19 +167,19 @@
"rename": "1.0.4", "rename": "1.0.4",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"sanitize-html": "2.14.0", "sanitize-html": "2.13.1",
"secure-json-parse": "3.0.2", "secure-json-parse": "2.7.0",
"sharp": "0.33.5", "sharp": "0.33.5",
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"systeminformation": "5.25.11", "systeminformation": "5.23.5",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.3", "tmp": "0.2.3",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typeorm": "0.3.20", "typeorm": "0.3.20",
"typescript": "5.7.3", "typescript": "5.6.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.7", "web-push": "3.6.7",
@@ -188,8 +188,8 @@
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.15", "@nestjs/platform-express": "10.4.7",
"@simplewebauthn/types": "12.0.0", "@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.37", "@swc/jest": "0.2.37",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
"@types/archiver": "6.0.3", "@types/archiver": "6.0.3",
@@ -204,15 +204,15 @@
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7", "@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.15", "@types/jsonld": "1.5.15",
"@types/jsrsasign": "10.5.15", "@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "22.13.4", "@types/node": "22.9.0",
"@types/nodemailer": "6.4.17", "@types/nodemailer": "6.4.16",
"@types/oauth": "0.9.6", "@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5", "@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2", "@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.11", "@types/pg": "8.11.10",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/random-seed": "0.3.5", "@types/random-seed": "0.3.5",
@@ -226,18 +226,18 @@
"@types/tmp": "0.2.6", "@types/tmp": "0.2.6",
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.4", "@types/web-push": "3.6.4",
"@types/ws": "8.5.14", "@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "8.24.0", "@typescript-eslint/parser": "7.17.0",
"aws-sdk-client-mock": "4.1.0", "aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.30.0",
"execa": "8.0.1", "execa": "8.0.1",
"fkill": "9.0.0", "fkill": "9.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-mock": "29.7.0", "jest-mock": "29.7.0",
"nodemon": "3.1.9", "nodemon": "3.1.7",
"pid-port": "1.0.2", "pid-port": "1.0.0",
"simple-oauth2": "5.1.0" "simple-oauth2": "5.1.0"
} }
} }

View File

@@ -73,7 +73,6 @@ type Source = {
proxyBypassHosts?: string[]; proxyBypassHosts?: string[];
allowedPrivateNetworks?: string[]; allowedPrivateNetworks?: string[];
disallowExternalApRedirect?: boolean;
maxFileSize?: number; maxFileSize?: number;
@@ -106,8 +105,8 @@ type Source = {
logging?: { logging?: {
sql?: { sql?: {
disableQueryTruncation?: boolean, disableQueryTruncation? : boolean,
enableQueryParamLogging?: boolean, enableQueryParamLogging? : boolean,
} }
} }
}; };
@@ -150,7 +149,6 @@ export type Config = {
proxySmtp: string | undefined; proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined; proxyBypassHosts: string[] | undefined;
allowedPrivateNetworks: string[] | undefined; allowedPrivateNetworks: string[] | undefined;
disallowExternalApRedirect: boolean;
maxFileSize: number; maxFileSize: number;
clusterLimit: number | undefined; clusterLimit: number | undefined;
id: string; id: string;
@@ -168,8 +166,8 @@ export type Config = {
signToActivityPubGet: boolean | undefined; signToActivityPubGet: boolean | undefined;
logging?: { logging?: {
sql?: { sql?: {
disableQueryTruncation?: boolean, disableQueryTruncation? : boolean,
enableQueryParamLogging?: boolean, enableQueryParamLogging? : boolean,
} }
} }
@@ -289,7 +287,6 @@ export function loadConfig(): Config {
proxySmtp: config.proxySmtp, proxySmtp: config.proxySmtp,
proxyBypassHosts: config.proxyBypassHosts, proxyBypassHosts: config.proxyBypassHosts,
allowedPrivateNetworks: config.allowedPrivateNetworks, allowedPrivateNetworks: config.allowedPrivateNetworks,
disallowExternalApRedirect: config.disallowExternalApRedirect ?? false,
maxFileSize: config.maxFileSize ?? 262144000, maxFileSize: config.maxFileSize ?? 262144000,
clusterLimit: config.clusterLimit, clusterLimit: config.clusterLimit,
outgoingAddress: config.outgoingAddress, outgoingAddress: config.outgoingAddress,

View File

@@ -43,7 +43,7 @@ export type CaptchaSetting = {
siteKey: string | null; siteKey: string | null;
secretKey: string | null; secretKey: string | null;
} }
}; }
export class CaptchaError extends Error { export class CaptchaError extends Error {
public readonly code: CaptchaErrorCode; public readonly code: CaptchaErrorCode;
@@ -59,11 +59,11 @@ export class CaptchaError extends Error {
export type CaptchaSaveSuccess = { export type CaptchaSaveSuccess = {
success: true; success: true;
}; }
export type CaptchaSaveFailure = { export type CaptchaSaveFailure = {
success: false; success: false;
error: CaptchaError; error: CaptchaError;
}; }
export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure; export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure;
type CaptchaResponse = { type CaptchaResponse = {

View File

@@ -173,8 +173,7 @@ export class DriveService {
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`; ?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
// for original // for original
const prefix = this.meta.objectStoragePrefix ? `${this.meta.objectStoragePrefix}/` : ''; const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
const key = `${prefix}${randomUUID()}${ext}`;
const url = `${ baseUrl }/${ key }`; const url = `${ baseUrl }/${ key }`;
// for alts // for alts
@@ -191,7 +190,7 @@ export class DriveService {
]; ];
if (alts.webpublic) { if (alts.webpublic) {
webpublicKey = `${prefix}webpublic-${randomUUID()}.${alts.webpublic.ext}`; webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
@@ -199,7 +198,7 @@ export class DriveService {
} }
if (alts.thumbnail) { if (alts.thumbnail) {
thumbnailKey = `${prefix}thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);

View File

@@ -164,13 +164,6 @@ export class EmailService {
available: boolean; available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; 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({ const exist = await this.userProfilesRepository.countBy({
emailVerified: true, emailVerified: true,
email: emailAddress, email: emailAddress,

View File

@@ -9,7 +9,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
export type FanoutTimelineName = ( export type FanoutTimelineName =
// home timeline // home timeline
| `homeTimeline:${string}` | `homeTimeline:${string}`
| `homeTimelineWithFiles:${string}` // only notes with files are included | `homeTimelineWithFiles:${string}` // only notes with files are included
@@ -37,7 +37,6 @@ export type FanoutTimelineName = (
// role timelines // role timelines
| `roleTimeline:${string}` // any notes are included | `roleTimeline:${string}` // any notes are included
);
@Injectable() @Injectable()
export class FanoutTimelineService { export class FanoutTimelineService {

View File

@@ -211,7 +211,7 @@ type SerializedAll<T> = {
type UndefinedAsNullAll<T> = { type UndefinedAsNullAll<T> = {
[K in keyof T]: T[K] extends undefined ? null : T[K]; [K in keyof T]: T[K] extends undefined ? null : T[K];
}; }
export interface InternalEventTypes { export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };

View File

@@ -16,7 +16,7 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from '@/core/activitypub/type.js'; import type { IObject } from '@/core/activitypub/type.js';
import type { Response } from 'node-fetch'; import type { Response } from 'node-fetch';
import type { URL } from 'node:url'; import type { URL } from 'node:url';
@@ -215,7 +215,7 @@ export class HttpRequestService {
} }
@bindThis @bindThis
public async getActivityJson(url: string, isLocalAddressAllowed = false, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise<IObject> { public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObject> {
const res = await this.send(url, { const res = await this.send(url, {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -232,7 +232,7 @@ export class HttpRequestService {
const finalUrl = res.url; // redirects may have been involved const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject; const activity = await res.json() as IObject;
assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail); assertActivityMatchesUrls(activity, [finalUrl]);
return activity; return activity;
} }

View File

@@ -492,8 +492,7 @@ export class MfmService {
appendChildren(nodes, body); appendChildren(nodes, body);
// Remove the unnecessary namespace const serialized = new XMLSerializer().serializeToString(body);
const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*<p xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">/, '<p>');
happyDOM.close().catch(err => {}); happyDOM.close().catch(err => {});

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Brackets, In, IsNull, Not } from 'typeorm'; import { Brackets, In } from 'typeorm';
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
@@ -189,27 +189,13 @@ export class NoteDeleteService {
}) as MiRemoteUser[]; }) 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 @bindThis
private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) {
this.apDeliverManagerService.deliverToFollowers(user, content); this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content); this.relayService.deliverToRelays(user, content);
this.apDeliverManagerService.deliverToUsers(user, content, [ const remoteUsers = await this.getMentionedRemoteUsers(note);
...await this.getMentionedRemoteUsers(note), for (const remoteUser of remoteUsers) {
...await this.getRenotedOrRepliedRemoteUsers(note), this.apDeliverManagerService.deliverToUser(user, content, remoteUser);
]); }
} }
} }

View File

@@ -220,7 +220,7 @@ export class SearchService {
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser');
if (this.config.fulltextSearch?.provider === 'sqlPgroonga') { if (this.config.fulltextSearch?.provider === 'sqlPgroonga') {
query.andWhere('note.text &@~ :q', { q }); query.andWhere('note.text &@ :q', { q });
} else { } else {
query.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` }); query.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` });
} }

View File

@@ -15,7 +15,7 @@ import { QueueService } from '@/core/QueueService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
export type UserWebhookPayload<T extends WebhookEventTypes> = export type UserWebhookPayload<T extends WebhookEventTypes> =
T extends 'note' | 'reply' | 'renote' | 'mention' ? { T extends 'note' | 'reply' | 'renote' |'mention' ? {
note: Packed<'Note'>, note: Packed<'Note'>,
} : } :
T extends 'follow' | 'unfollow' ? { T extends 'follow' | 'unfollow' ? {

View File

@@ -38,14 +38,6 @@ export class UtilityService {
return this.punyHost(uri) === this.toPuny(this.config.host); 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 @bindThis
public isBlockedHost(blockedHosts: string[], host: string | null): boolean { public isBlockedHost(blockedHosts: string[], host: string | null): boolean {
if (host == null) return false; if (host == null) return false;

View File

@@ -127,11 +127,11 @@ export class WebAuthnService {
const { registrationInfo } = verification; const { registrationInfo } = verification;
return { return {
credentialID: registrationInfo.credential.id, credentialID: registrationInfo.credentialID,
credentialPublicKey: registrationInfo.credential.publicKey, credentialPublicKey: registrationInfo.credentialPublicKey,
attestationObject: registrationInfo.attestationObject, attestationObject: registrationInfo.attestationObject,
fmt: registrationInfo.fmt, fmt: registrationInfo.fmt,
counter: registrationInfo.credential.counter, counter: registrationInfo.counter,
userVerified: registrationInfo.userVerified, userVerified: registrationInfo.userVerified,
credentialDeviceType: registrationInfo.credentialDeviceType, credentialDeviceType: registrationInfo.credentialDeviceType,
credentialBackedUp: registrationInfo.credentialBackedUp, credentialBackedUp: registrationInfo.credentialBackedUp,
@@ -212,9 +212,9 @@ export class WebAuthnService {
expectedChallenge: challenge, expectedChallenge: challenge,
expectedOrigin: relyingParty.origin, expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId, expectedRPID: relyingParty.rpId,
credential: { authenticator: {
id: key.id, credentialID: key.id,
publicKey: Buffer.from(key.publicKey, 'base64url'), credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter, counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
}, },
@@ -292,9 +292,9 @@ export class WebAuthnService {
expectedChallenge: challenge, expectedChallenge: challenge,
expectedOrigin: relyingParty.origin, expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId, expectedRPID: relyingParty.rpId,
credential: { authenticator: {
id: key.id, credentialID: key.id,
publicKey: Buffer.from(key.publicKey, 'base64url'), credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter, counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
}, },

View File

@@ -196,25 +196,6 @@ export class ApDeliverManagerService {
await manager.execute(); 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<void> {
const manager = new DeliverManager(
this.userEntityService,
this.followingsRepository,
this.queueService,
actor,
activity,
);
for (const to of targets) manager.addDirectRecipe(to);
await manager.execute();
}
@bindThis @bindThis
public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager { public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager {
return new DeliverManager( return new DeliverManager(

View File

@@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from './type.js'; import type { IObject } from './type.js';
type Request = { type Request = {
@@ -185,7 +185,7 @@ export class ApRequestService {
* @param url URL to fetch * @param url URL to fetch
*/ */
@bindThis @bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict, followAlternate?: boolean): Promise<unknown> { public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> {
const _followAlternate = followAlternate ?? true; const _followAlternate = followAlternate ?? true;
const keypair = await this.userKeypairService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
@@ -243,7 +243,7 @@ export class ApRequestService {
if (alternate) { if (alternate) {
const href = alternate.getAttribute('href'); const href = alternate.getAttribute('href');
if (href && this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) { if (href && this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) {
return await this.signedGet(href, user, allowSoftfail, false); return await this.signedGet(href, user, false);
} }
} }
} catch (e) { } catch (e) {
@@ -258,7 +258,7 @@ export class ApRequestService {
const finalUrl = res.url; // redirects may have been involved const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject; const activity = await res.json() as IObject;
assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail); assertActivityMatchesUrls(activity, [finalUrl]);
return activity; return activity;
} }

View File

@@ -21,7 +21,6 @@ import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js'; import { ApRequestService } from './ApRequestService.js';
import type { IObject, ICollection, IOrderedCollection } from './type.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
export class Resolver { export class Resolver {
private history: Set<string>; private history: Set<string>;
@@ -73,7 +72,7 @@ export class Resolver {
} }
@bindThis @bindThis
public async resolve(value: string | IObject, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise<IObject> { public async resolve(value: string | IObject): Promise<IObject> {
if (typeof value !== 'string') { if (typeof value !== 'string') {
return value; return value;
} }
@@ -109,8 +108,8 @@ export class Resolver {
} }
const object = (this.user const object = (this.user
? await this.apRequestService.signedGet(value, this.user, allowSoftfail) as IObject ? await this.apRequestService.signedGet(value, this.user) as IObject
: await this.httpRequestService.getActivityJson(value, undefined, allowSoftfail)) as IObject; : await this.httpRequestService.getActivityJson(value)) as IObject;
if ( if (
Array.isArray(object['@context']) ? Array.isArray(object['@context']) ?
@@ -119,7 +118,19 @@ export class Resolver {
) { ) {
throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response'); 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; return object;
} }

View File

@@ -4,124 +4,18 @@
*/ */
import type { IObject } from '../type.js'; import type { IObject } from '../type.js';
export enum FetchAllowSoftFailMask { export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
// Allow no softfail flags const hosts = urls.map(it => new URL(it).host);
Strict = 0,
// The values in tuple (requestUrl, finalUrl, objectId) are not all identical const idOk = activity.id !== undefined && hosts.includes(new URL(activity.id).host);
//
// This condition is common for user-initiated lookups but should not be allowed in federation loop // technically `activity.url` could be an `ApObject = IObject |
// // string | (IObject | string)[]`, but if it's a complicated thing
// Allow variations: // and the `activity.id` doesn't match, I think we're fine
// good example: https://alice.example.com/@user -> https://alice.example.com/user/:userId // rejecting the activity
// problematic example: https://alice.example.com/redirect?url=https://bad.example.com/ -> https://bad.example.com/ -> https://alice.example.com/somethingElse const urlOk = typeof(activity.url) === 'string' && hosts.includes(new URL(activity.url).host);
NonCanonicalId = 1 << 0,
// Allow the final object to be at most one subdomain deeper than the request URL, similar to SPF relaxed alignment if (!idOk && !urlOk) {
// throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${activity?.url}) match location(${urls})`);
// 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;
}
if (`.${candidateHost}`.endsWith(`.${requestHost}`)) {
return FetchAllowSoftFailMask.MisalignedOrigin;
}
return FetchAllowSoftFailMask.CrossOrigin;
}
// 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;
} }

View File

@@ -97,7 +97,6 @@ export class MetaEntityService {
enableTurnstile: instance.enableTurnstile, enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey, turnstileSiteKey: instance.turnstileSiteKey,
enableTestcaptcha: instance.enableTestcaptcha, enableTestcaptcha: instance.enableTestcaptcha,
googleAnalyticsMeasurementId: instance.googleAnalyticsMeasurementId,
swPublickey: instance.swPublicKey, swPublickey: instance.swPublicKey,
themeColor: instance.themeColor, themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png', mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',

View File

@@ -57,14 +57,12 @@ const ajv = new Ajv();
function isLocalUser(user: MiUser): user is MiLocalUser; function isLocalUser(user: MiUser): user is MiLocalUser;
function isLocalUser<T extends { host: MiUser['host'] }>(user: T): user is (T & { host: null; }); function isLocalUser<T extends { host: MiUser['host'] }>(user: T): user is (T & { host: null; });
function isLocalUser(user: MiUser | { host: MiUser['host'] }): boolean { function isLocalUser(user: MiUser | { host: MiUser['host'] }): boolean {
return user.host == null; return user.host == null;
} }
function isRemoteUser(user: MiUser): user is MiRemoteUser; function isRemoteUser(user: MiUser): user is MiRemoteUser;
function isRemoteUser<T extends { host: MiUser['host'] }>(user: T): user is (T & { host: string; }); function isRemoteUser<T extends { host: MiUser['host'] }>(user: T): user is (T & { host: string; });
function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean { function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
return !isLocalUser(user); return !isLocalUser(user);
} }
@@ -80,7 +78,7 @@ export type UserRelation = {
isBlocked: boolean isBlocked: boolean
isMuted: boolean isMuted: boolean
isRenoteMuted: boolean isRenoteMuted: boolean
}; }
@Injectable() @Injectable()
export class UserEntityService implements OnModuleInit { export class UserEntityService implements OnModuleInit {

View File

@@ -143,7 +143,7 @@ type OfSchema = {
readonly anyOf?: ReadonlyArray<Schema>; readonly anyOf?: ReadonlyArray<Schema>;
readonly oneOf?: ReadonlyArray<Schema>; readonly oneOf?: ReadonlyArray<Schema>;
readonly allOf?: ReadonlyArray<Schema>; readonly allOf?: ReadonlyArray<Schema>;
}; }
export interface Schema extends OfSchema { export interface Schema extends OfSchema {
readonly type?: TypeStringef; readonly type?: TypeStringef;
@@ -217,7 +217,7 @@ type ObjectSchemaTypeDef<p extends Schema> =
: :
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> : p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any; any
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>; type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;

View File

@@ -4,7 +4,7 @@
*/ */
export type JsonValue = JsonArray | JsonObject | string | number | boolean | null; 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 type JsonArray = JsonValue[];
export function isJsonObject(value: JsonValue | undefined): value is JsonObject { export function isJsonObject(value: JsonValue | undefined): value is JsonObject {

View File

@@ -658,10 +658,4 @@ export class MiMeta {
default: '{}', default: '{}',
}) })
public federationHosts: string[]; public federationHosts: string[];
@Column('varchar', {
length: 64,
nullable: true,
})
public googleAnalyticsMeasurementId: string | null;
} }

View File

@@ -90,10 +90,6 @@ export type MiNotification = {
type: 'login'; type: 'login';
id: string; id: string;
createdAt: string; createdAt: string;
} | {
type: 'createToken';
id: string;
createdAt: string;
} | { } | {
type: 'app'; type: 'app';
id: string; id: string;

View File

@@ -288,24 +288,24 @@ export class MiUser {
export type MiLocalUser = MiUser & { export type MiLocalUser = MiUser & {
host: null; host: null;
uri: null; uri: null;
}; }
export type MiPartialLocalUser = Partial<MiUser> & { export type MiPartialLocalUser = Partial<MiUser> & {
id: MiUser['id']; id: MiUser['id'];
host: null; host: null;
uri: null; uri: null;
}; }
export type MiRemoteUser = MiUser & { export type MiRemoteUser = MiUser & {
host: string; host: string;
uri: string; uri: string;
}; }
export type MiPartialRemoteUser = Partial<MiUser> & { export type MiPartialRemoteUser = Partial<MiUser> & {
id: MiUser['id']; id: MiUser['id'];
host: string; host: string;
uri: string; uri: string;
}; }
export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
export const passwordSchema = { type: 'string', minLength: 1 } as const; export const passwordSchema = { type: 'string', minLength: 1 } as const;

View File

@@ -119,10 +119,6 @@ export const packedMetaLiteSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
googleAnalyticsMeasurementId: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: { swPublickey: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,

View File

@@ -332,16 +332,6 @@ export const packedNotificationSchema = {
enum: ['login'], enum: ['login'],
}, },
}, },
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['createToken'],
},
},
}, { }, {
type: 'object', type: 'object',
properties: { properties: {

View File

@@ -92,7 +92,7 @@ const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
export type LoggerProps = { export type LoggerProps = {
disableQueryTruncation?: boolean; disableQueryTruncation?: boolean;
enableQueryParamLogging?: boolean; enableQueryParamLogging?: boolean;
}; }
function highlightSql(sql: string) { function highlightSql(sql: string) {
return highlight.highlight(sql, { return highlight.highlight(sql, {

View File

@@ -29,7 +29,7 @@ export type ModeratorInactivityEvaluationResult = {
isModeratorsInactive: boolean; isModeratorsInactive: boolean;
inactiveModerators: MiUser[]; inactiveModerators: MiUser[];
remainingTime: ModeratorInactivityRemainingTime; remainingTime: ModeratorInactivityRemainingTime;
}; }
export type ModeratorInactivityRemainingTime = { export type ModeratorInactivityRemainingTime = {
time: number; time: number;

View File

@@ -107,12 +107,12 @@ export class InboxProcessorService implements OnApplicationShutdown {
// それでもわからなければ終了 // それでもわからなければ終了
if (authUser == null) { if (authUser == null) {
throw new Bull.UnrecoverableError(`skip: failed to resolve user ${getApId(activity.actor)}`); throw new Bull.UnrecoverableError('skip: failed to resolve user');
} }
// publicKey がなくても終了 // publicKey がなくても終了
if (authUser.key == null) { if (authUser.key == null) {
throw new Bull.UnrecoverableError(`skip: failed to resolve user publicKey ${getApId(activity.actor)}`); throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey');
} }
// HTTP-Signatureの検証 // HTTP-Signatureの検証

View File

@@ -38,7 +38,7 @@ export type RelationshipJobData = {
silent?: boolean; silent?: boolean;
requestId?: string; requestId?: string;
withReplies?: boolean; withReplies?: boolean;
}; }
export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T]; export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T];
@@ -61,11 +61,11 @@ export type DbJobMap = {
importUserLists: DbUserImportJobData; importUserLists: DbUserImportJobData;
importCustomEmojis: DbUserImportJobData; importCustomEmojis: DbUserImportJobData;
deleteAccount: DbUserDeleteJobData; deleteAccount: DbUserDeleteJobData;
}; }
export type DbJobDataWithUser = { export type DbJobDataWithUser = {
user: ThinUser; user: ThinUser;
}; }
export type DbExportFollowingData = { export type DbExportFollowingData = {
user: ThinUser; user: ThinUser;
@@ -75,7 +75,7 @@ export type DbExportFollowingData = {
export type DBExportAntennasData = { export type DBExportAntennasData = {
user: ThinUser user: ThinUser
}; }
export type DbUserDeleteJobData = { export type DbUserDeleteJobData = {
user: ThinUser; user: ThinUser;
@@ -91,7 +91,7 @@ export type DbUserImportJobData = {
export type DBAntennaImportJobData = { export type DBAntennaImportJobData = {
user: ThinUser, user: ThinUser,
antenna: Antenna antenna: Antenna
}; }
export type DbUserImportToDbJobData = { export type DbUserImportToDbJobData = {
user: ThinUser; user: ThinUser;

View File

@@ -103,43 +103,6 @@ export class ServerService implements OnApplicationShutdown {
serve: false, 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.apiServerService.createServer, { prefix: '/api' });
fastify.register(this.openApiServerService.createServer); fastify.register(this.openApiServerService.createServer);
fastify.register(this.fileServerService.createServer); fastify.register(this.fileServerService.createServer);

View File

@@ -122,7 +122,7 @@ export type IEndpointMeta = (Omit<IEndpointMetaBase, 'requireCrential' | 'requir
}) | (Omit<IEndpointMetaBase, 'requireAdmin' | 'kind'> & { }) | (Omit<IEndpointMetaBase, 'requireAdmin' | 'kind'> & {
requireAdmin: true, requireAdmin: true,
kind: (typeof permissions)[number], kind: (typeof permissions)[number],
}); })
export interface IEndpoint { export interface IEndpoint {
name: string; name: string;

View File

@@ -73,10 +73,6 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
googleAnalyticsMeasurementId: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: { swPublickey: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
@@ -516,7 +512,6 @@ export const meta = {
}, },
federation: { federation: {
type: 'string', type: 'string',
enum: ['all', 'specified', 'none'],
optional: false, nullable: false, optional: false, nullable: false,
}, },
federationHosts: { federationHosts: {
@@ -576,7 +571,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableTurnstile: instance.enableTurnstile, enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey, turnstileSiteKey: instance.turnstileSiteKey,
enableTestcaptcha: instance.enableTestcaptcha, enableTestcaptcha: instance.enableTestcaptcha,
googleAnalyticsMeasurementId: instance.googleAnalyticsMeasurementId,
swPublickey: instance.swPublicKey, swPublickey: instance.swPublicKey,
themeColor: instance.themeColor, themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl, mascotImageUrl: instance.mascotImageUrl,

View File

@@ -84,7 +84,6 @@ export const paramDef = {
turnstileSiteKey: { type: 'string', nullable: true }, turnstileSiteKey: { type: 'string', nullable: true },
turnstileSecretKey: { type: 'string', nullable: true }, turnstileSecretKey: { type: 'string', nullable: true },
enableTestcaptcha: { type: 'boolean' }, enableTestcaptcha: { type: 'boolean' },
googleAnalyticsMeasurementId: { type: 'string', nullable: true },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' }, setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -372,12 +371,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableTestcaptcha = ps.enableTestcaptcha; 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) { if (ps.sensitiveMediaDetection !== undefined) {
set.sensitiveMediaDetection = ps.sensitiveMediaDetection; set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
} }

View File

@@ -20,7 +20,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
export const meta = { export const meta = {
tags: ['federation'], tags: ['federation'],
@@ -54,6 +53,11 @@ export const meta = {
code: 'RESPONSE_INVALID', code: 'RESPONSE_INVALID',
id: '70193c39-54f3-4813-82f0-70a680f7495b', 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: { noSuchObject: {
message: 'No such object.', message: 'No such object.',
code: 'NO_SUCH_OBJECT', code: 'NO_SUCH_OBJECT',
@@ -149,8 +153,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// リモートから一旦オブジェクトフェッチ // リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver(); const resolver = this.apResolverService.createResolver();
// 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).catch((err) => {
const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => {
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
switch (err.id) { switch (err.id) {
// resolve // resolve
@@ -162,7 +165,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
case '09d79f9e-64f1-4316-9cfa-e75c4d091574': case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
throw new ApiError(meta.errors.federationNotAllowed); throw new ApiError(meta.errors.federationNotAllowed);
case '72180409-793c-4973-868e-5a118eb5519b': case '72180409-793c-4973-868e-5a118eb5519b':
case 'ad2dc287-75c1-44c4-839d-3d2e64576675':
throw new ApiError(meta.errors.responseInvalid); throw new ApiError(meta.errors.responseInvalid);
case 'fd93c2fa-69a8-440f-880b-bf178e0ec877':
throw new ApiError(meta.errors.responseInvalidIdHostNotMatch);
// resolveLocal // resolveLocal
case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8': case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':

View File

@@ -96,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.userFollowingService.unfollow(follower, followee); await this.userFollowingService.unfollow(follower, followee);
return await this.userEntityService.pack(follower.id, me); return await this.userEntityService.pack(followee.id, me);
}); });
} }
} }

View File

@@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AccessTokensRepository } from '@/models/_.js'; import type { AccessTokensRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@@ -51,7 +50,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private accessTokensRepository: AccessTokensRepository, private accessTokensRepository: AccessTokensRepository,
private idService: IdService, private idService: IdService,
private notificationService: NotificationService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
// Generate access token // Generate access token
@@ -73,9 +71,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
permission: ps.permission, permission: ps.permission,
}); });
// アクセストークンが生成されたことを通知
this.notificationService.createNotification(me.id, 'createToken', {});
return { return {
token: accessToken, token: accessToken,
}; };

View File

@@ -210,15 +210,9 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
spec.paths['/' + endpoint.name] = { spec.paths['/' + endpoint.name] = {
...(endpoint.meta.allowGet ? { ...(endpoint.meta.allowGet ? {
get: { get: info,
...info,
operationId: 'get___' + info.operationId,
},
} : {}), } : {}),
post: { post: info,
...info,
operationId: 'post___' + info.operationId,
},
}; };
} }

View File

@@ -82,8 +82,8 @@ export default abstract class Channel {
this.connection = connection; this.connection = connection;
} }
public send(payload: { type: string, body: JsonValue }): void; public send(payload: { type: string, body: JsonValue }): void
public send(type: string, payload: JsonValue): void; public send(type: string, payload: JsonValue): void
@bindThis @bindThis
public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) { public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string); const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
@@ -108,4 +108,4 @@ export type MiChannelService<T extends boolean> = {
requireCredential: T; requireCredential: T;
kind: T extends true ? string : string | null | undefined; kind: T extends true ? string : string | null | undefined;
create: (id: string, connection: Connection) => Channel; create: (id: string, connection: Connection) => Channel;
}; }

View File

@@ -114,17 +114,13 @@
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); 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 = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg> document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
<div class="message">${title}</div> <div class="message">読み込みに失敗しました</div>
<div class="submessage">Failed to initialize Misskey</div>
<div class="submessage">Error Code: ${code}</div> <div class="submessage">Error Code: ${code}</div>
<button onclick="location.reload(!0)"> <button onclick="location.reload(!0)">
<div>${reload}</div> <div>リロード</div>
<div><small>Reload</small></div>
</button>`; </button>`;
addStyle(` addStyle(`
#misskey_app, #misskey_app,

View File

@@ -151,22 +151,6 @@
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); 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'); let errorsElement = document.getElementById('errors');
if (!errorsElement) { if (!errorsElement) {
@@ -176,32 +160,32 @@
<path d="M12 9v2m0 4v.01"></path> <path d="M12 9v2m0 4v.01"></path>
<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"></path> <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"></path>
</svg> </svg>
<h1>${messages.title}</h1> <h1>Failed to load<br>読み込みに失敗しました</h1>
<button class="button-big" onclick="location.reload(true);"> <button class="button-big" onclick="location.reload(true);">
<span class="button-label-big">${reload}</span> <span class="button-label-big">Reload / リロード</span>
</button> </button>
<p><b>${messages.solution}</b></p> <p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p>
<p>${messages.solution1}</p> <p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
<p>${messages.solution2}</p> <p>Disable an adblocker / アドブロッカーを無効にする</p>
<p>${messages.solution3}</p> <p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
<p>${messages.solution4}</p> <p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
<details style="color: #86b300;"> <details style="color: #86b300;">
<summary>${messages.otherOption}</summary> <summary>Other options / その他のオプション</summary>
<a href="/flush"> <a href="/flush">
<button class="button-small"> <button class="button-small">
<span class="button-label-small">${messages.otherOption1}</span> <span class="button-label-small">Clear preferences and cache</span>
</button> </button>
</a> </a>
<br> <br>
<a href="/cli"> <a href="/cli">
<button class="button-small"> <button class="button-small">
<span class="button-label-small">${messages.otherOption2}</span> <span class="button-label-small">Start the simple client</span>
</button> </button>
</a> </a>
<br> <br>
<a href="/bios"> <a href="/bios">
<button class="button-small"> <button class="button-small">
<span class="button-label-small">${messages.otherOption3}</span> <span class="button-label-small">Start the repair tool</span>
</button> </button>
</a> </a>
</details> </details>

View File

@@ -5,107 +5,112 @@
*/ */
* { * {
font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
} }
#misskey_app, #misskey_app,
#splash { #splash {
display: none !important; display: none !important;
} }
body, body,
html { html {
background-color: #222; background-color: #222;
color: #dfddcc; color: #dfddcc;
justify-content: center; justify-content: center;
margin: auto; margin: auto;
padding: 10px; padding: 10px;
text-align: center; text-align: center;
} }
button { button {
border-radius: 999px; border-radius: 999px;
padding: 0px 12px 0px 12px; padding: 0px 12px 0px 12px;
border: none; border: none;
cursor: pointer; cursor: pointer;
margin-bottom: 12px; margin-bottom: 12px;
} }
.button-big { .button-big {
background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0)); background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
line-height: 50px; line-height: 50px;
} }
.button-big:hover { .button-big:hover {
background: rgb(153, 204, 0); background: rgb(153, 204, 0);
} }
.button-small { .button-small {
background: #444; background: #444;
line-height: 40px; line-height: 40px;
} }
.button-small:hover { .button-small:hover {
background: #555; background: #555;
} }
.button-label-big { .button-label-big {
color: #222; color: #222;
font-weight: bold; font-weight: bold;
font-size: 1.2em; font-size: 20px;
padding: 12px; padding: 12px;
} }
.button-label-small { .button-label-small {
color: rgb(153, 204, 0); color: rgb(153, 204, 0);
font-size: 16px; font-size: 16px;
padding: 12px; padding: 12px;
} }
a { a {
color: rgb(134, 179, 0); color: rgb(134, 179, 0);
text-decoration: none; text-decoration: none;
} }
p, p,
li { li {
font-size: 16px; font-size: 16px;
}
.dont-worry,
#msg {
font-size: 18px;
} }
.icon-warning { .icon-warning {
color: #dec340; color: #dec340;
height: 4rem; height: 4rem;
padding-top: 2rem; padding-top: 2rem;
} }
h1 { h1 {
font-size: 1.5em; font-size: 32px;
margin: 1em;
} }
code { code {
display: block; display: block;
font-family: Fira, FiraCode, monospace; font-family: Fira, FiraCode, monospace;
background: #333; background: #333;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
max-width: 40rem; max-width: 40rem;
border-radius: 10px; border-radius: 10px;
justify-content: center; justify-content: center;
margin: auto; margin: auto;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
#errorInfo summary { summary {
cursor: pointer; cursor: pointer;
} }
#errorInfo summary>* { summary > * {
display: inline; display: inline;
white-space: pre-wrap;
} }
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
#errorInfo { details {
width: 50%; width: 50%;
} }
} }

View File

@@ -1,40 +0,0 @@
/*
* 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];
}
}
});
})();

View File

@@ -2,15 +2,15 @@ doctype html
// //
- -
_____ _ _ _____ _ _
| |_|___ ___| |_ ___ _ _ | |_|___ ___| |_ ___ _ _
| | | | |_ -|_ -| '_| -_| | | | | | | |_ -|_ -| '_| -_| | |
|_|_|_|_|___|___|_,_|___|_ | |_|_|_|_|___|___|_,_|___|_ |
|___| |___|
Thank you for using Misskey! Thank you for using Misskey!
If you are reading this message... how about joining the development? If you are reading this message... how about joining the development?
https://github.com/misskey-dev/misskey https://github.com/misskey-dev/misskey
html html
@@ -27,45 +27,39 @@ html
style style
include ../error.css include ../error.css
script
include ../error.js
body 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") 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(stroke="none", d="M0 0h24v24H0z", fill="none")
path(d="M12 9v2m0 4v.01") 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") 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(data-i18n="title") Failed to initialize Misskey h1 An error has occurred!
button.button-big(onclick="location.reload();") button.button-big(onclick="location.reload();")
span.button-label-big(data-i18n-reload) Reload span.button-label-big Refresh
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. p.dont-worry Don't worry, it's (probably) not your fault.
p If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.
div#errors div#errors
code. code.
ERROR CODE: #{code} ERROR CODE: #{code}
ERROR ID: #{id} ERROR ID: #{id}
p p You may also try the following options:
b(data-i18n="solution") The following actions may solve the problem.
p(data-i18n="solution1") Update your os and browser p Update your os and browser.
p(data-i18n="solution2") Disable an adblocker p Disable an adblocker.
p(data-i18n="solution3") Clear your browser cache
p(data-i18n="solution4") (Tor Browser) Set dom.webaudio.enabled to true
details(style="color: #86b300;") a(href="/flush")
summary(data-i18n="otherOption") Other options button.button-small
a(href="/flush") span.button-label-small Clear preferences and cache
button.button-small br
span.button-label-small(data-i18n="otherOption1") Clear preferences and cache a(href="/cli")
br button.button-small
a(href="/cli") span.button-label-small Start the simple client
button.button-small br
span.button-label-small(data-i18n="otherOption2") Start the simple client a(href="/bios")
br button.button-small
a(href="/bios") span.button-label-small Start the repair tool
button.button-small
span.button-label-small(data-i18n="otherOption3") Start the repair tool

View File

@@ -18,7 +18,6 @@
* achievementEarned - 実績を獲得 * achievementEarned - 実績を獲得
* exportCompleted - エクスポートが完了 * exportCompleted - エクスポートが完了
* login - ログイン * login - ログイン
* createToken - トークン作成
* app - アプリ通知 * app - アプリ通知
* test - テスト通知(サーバー側) * test - テスト通知(サーバー側)
*/ */
@@ -37,7 +36,6 @@ export const notificationTypes = [
'achievementEarned', 'achievementEarned',
'exportCompleted', 'exportCompleted',
'login', 'login',
'createToken',
'app', 'app',
'test', 'test',
] as const; ] as const;

View File

@@ -139,99 +139,29 @@ describe('Note', () => {
}); });
describe('Deletion', () => { describe('Deletion', () => {
describe('Check Delete is delivered', () => { describe('Check Delete consistency', () => {
describe('To followers', () => { let carol: LoginUser;
let carol: LoginUser;
beforeAll(async () => { beforeAll(async () => {
carol = await createAccount('a.test'); carol = await createAccount('a.test');
await carol.client.request('following/create', { userId: bobInA.id }); await carol.client.request('following/create', { userId: bobInA.id });
await sleep(); 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();
});
}); });
describe('To renoted and not followed user', () => { test('Delete is derivered to followers', async () => {
test('Check', async () => { const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; const noteInA = await resolveRemoteNote('b.test', note.id, carol);
const noteInA = await resolveRemoteNote('b.test', note.id, alice); await bob.client.request('notes/delete', { noteId: note.id });
await alice.client.request('notes/create', { renoteId: noteInA.id }); await sleep();
await sleep();
await bob.client.request('notes/delete', { noteId: note.id }); await rejects(
await sleep(); async () => await carol.client.request('notes/show', { noteId: noteInA.id }),
(err: any) => {
await rejects( strictEqual(err.code, 'NO_SUCH_NOTE');
async () => await alice.client.request('notes/show', { noteId: noteInA.id }), return true;
(err: any) => { },
strictEqual(err.code, 'NO_SUCH_NOTE'); );
return true;
},
);
});
});
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;
},
);
});
});
/**
* 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;
},
);
});
}); });
}); });

View File

@@ -22,7 +22,7 @@ export type LoginUser = SigninResponse & {
client: Misskey.api.APIClient; client: Misskey.api.APIClient;
username: string; username: string;
password: string; password: string;
}; }
/** used for avoiding overload and some endpoints */ /** used for avoiding overload and some endpoints */
export type Request = < export type Request = <

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://swc.rs/schema.json", "$schema": "https://json.schemastore.org/swcrc",
"jsc": { "jsc": {
"parser": { "parser": {
"syntax": "typescript", "syntax": "typescript",

View File

@@ -10,13 +10,13 @@ import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet
import type { SimpleGetResponse } from '../utils.js'; import type { SimpleGetResponse } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
// Request Accept in lowercase // Request Accept
const ONLY_AP = 'application/activity+json'; const ONLY_AP = 'application/activity+json';
const PREFER_AP = 'application/activity+json, */*'; const PREFER_AP = 'application/activity+json, */*';
const PREFER_HTML = 'text/html, */*'; const PREFER_HTML = 'text/html, */*';
const UNSPECIFIED = '*/*'; const UNSPECIFIED = '*/*';
// Response Content-Type in lowercase // Response Content-Type
const AP = 'application/activity+json; charset=utf-8'; const AP = 'application/activity+json; charset=utf-8';
const HTML = 'text/html; charset=utf-8'; const HTML = 'text/html; charset=utf-8';
const JSON_UTF8 = 'application/json; charset=utf-8'; const JSON_UTF8 = 'application/json; charset=utf-8';
@@ -44,8 +44,7 @@ describe('Webリソース', () => {
const { path, accept, cookie, type } = param; const { path, accept, cookie, type } = param;
const res = await simpleGet(path, accept, cookie); const res = await simpleGet(path, accept, cookie);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
// Header values are case-insensitive assert.strictEqual(res.type, type ?? HTML);
assert.strictEqual(res.type?.toLowerCase(), (type ?? HTML).toLowerCase());
return res; return res;
}; };
@@ -96,7 +95,8 @@ describe('Webリソース', () => {
describe.each([ describe.each([
{ path: '/', type: HTML }, { path: '/', type: HTML },
{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。" { path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"
{ path: '/api-doc', type: HTML }, // fastify-static gives charset=UTF-8 instead of utf-8 and that's okay
{ path: '/api-doc', type: 'text/html; charset=UTF-8' },
{ path: '/api.json', type: JSON_UTF8 }, { path: '/api.json', type: JSON_UTF8 },
{ path: '/api-console', type: HTML }, { path: '/api-console', type: HTML },
{ path: '/_info_card_', type: HTML }, { path: '/_info_card_', type: HTML },

View File

@@ -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 === bobNote2.id), true);
assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false);
assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
}, 1000 * 30); }, 1000 * 15);
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => { test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);

View File

@@ -24,13 +24,13 @@ describe('MfmService', () => {
describe('toHtml', () => { describe('toHtml', () => {
test('br', () => { test('br', () => {
const input = 'foo\nbar\nbaz'; const input = 'foo\nbar\nbaz';
const output = '<p><span>foo<br />bar<br />baz</span></p>'; const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output); assert.equal(mfmService.toHtml(mfm.parse(input)), output);
}); });
test('br alt', () => { test('br alt', () => {
const input = 'foo\r\nbar\rbaz'; const input = 'foo\r\nbar\rbaz';
const output = '<p><span>foo<br />bar<br />baz</span></p>'; const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output); assert.equal(mfmService.toHtml(mfm.parse(input)), output);
}); });

View File

@@ -8,8 +8,6 @@ import httpSignature from '@peertube/http-signature';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.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) => { export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
return { return {
@@ -26,10 +24,6 @@ export const buildParsedSignature = (signingString: string, signature: string, a
}; };
}; };
function cartesianProduct<T, U>(a: T[], b: U[]): [T, U][] {
return a.flatMap(a => b.map(b => [a, b] as [T, U]));
}
describe('ap-request', () => { describe('ap-request', () => {
test('createSignedPost with verify', async () => { test('createSignedPost with verify', async () => {
const keypair = await genRsaKeyPair(); const keypair = await genRsaKeyPair();
@@ -64,123 +58,4 @@ describe('ap-request', () => {
const result = httpSignature.verifySignature(parsed, keypair.publicKey); const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true); 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');
});
}); });

View File

@@ -12,12 +12,12 @@
"dependencies": { "dependencies": {
"@discordapp/twemoji": "15.1.0", "@discordapp/twemoji": "15.1.0",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2", "@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.4", "@rollup/pluginutils": "5.1.3",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz", "@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.0",
"@vue/compiler-sfc": "3.5.13", "@vue/compiler-sfc": "3.5.12",
"astring": "1.9.0", "astring": "1.9.0",
"buraha": "0.0.1", "buraha": "0.0.1",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
@@ -25,46 +25,47 @@
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"frontend-shared": "workspace:*", "frontend-shared": "workspace:*",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.34.8", "rollup": "4.26.0",
"sass": "1.85.0", "sass": "1.79.4",
"shiki": "3.0.0", "shiki": "1.22.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typescript": "5.7.3", "typescript": "5.6.3",
"uuid": "11.1.0", "uuid": "10.0.0",
"json5": "2.2.3", "json5": "2.2.3",
"vite": "6.1.1", "vite": "5.4.11",
"vue": "3.5.13" "vue": "3.5.12"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/summaly": "5.2.0", "@misskey-dev/summaly": "5.1.0",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
"@types/node": "22.13.5", "@types/node": "22.9.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.5.14", "@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.24.1", "@types/ws": "8.5.13",
"@typescript-eslint/parser": "8.24.1", "@typescript-eslint/eslint-plugin": "7.17.0",
"@vitest/coverage-v8": "3.0.6", "@typescript-eslint/parser": "7.17.0",
"@vue/runtime-core": "3.5.13", "@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.12",
"acorn": "8.14.0", "acorn": "8.14.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.31.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.2",
"happy-dom": "17.1.4", "happy-dom": "10.0.3",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"msw": "2.7.1", "msw": "2.6.4",
"nodemon": "3.1.9", "nodemon": "3.1.7",
"prettier": "3.5.2", "prettier": "3.3.3",
"start-server-and-test": "2.0.10", "start-server-and-test": "2.0.8",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "2.2.4", "vue-component-type-helpers": "2.1.10",
"vue-eslint-parser": "9.4.3", "vue-eslint-parser": "9.4.3",
"vue-tsc": "2.2.4" "vue-tsc": "2.1.10"
} }
} }

View File

@@ -21,7 +21,7 @@ export type MiPostMessageEvent<T extends PostMessageEventType = PostMessageEvent
type: T; type: T;
iframeId?: string; iframeId?: string;
payload?: PostMessageEventPayload[T]; payload?: PostMessageEventPayload[T];
}; }
let defaultIframeId: string | null = null; let defaultIframeId: string | null = null;

View File

@@ -98,12 +98,4 @@ export default [
'vue/attribute-hyphenation': ['error', 'never'], 'vue/attribute-hyphenation': ['error', 'never'],
}, },
}, },
{
ignores: [
// TODO: Error while loading rule '@typescript-eslint/naming-convention': Cannot use 'in' operator to search for 'type' in undefined のため一時的に無効化
// See https://github.com/misskey-dev/misskey/pull/15311
'js/i18n.ts',
'js-built/',
],
},
]; ];

View File

@@ -69,7 +69,6 @@ export const notificationTypes = [
'achievementEarned', 'achievementEarned',
'exportCompleted', 'exportCompleted',
'login', 'login',
'createToken',
'test', 'test',
'app', 'app',
] as const; ] as const;

View File

@@ -6,7 +6,7 @@
//#region Embed関連の定義 //#region Embed関連の定義
/** 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる) */ /** 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる) */
export const embeddableEntities = [ const embeddableEntities = [
'notes', 'notes',
'user-timeline', 'user-timeline',
'clips', 'clips',

View File

@@ -9,10 +9,10 @@ export type UnicodeEmojiDef = {
name: string; name: string;
char: string; char: string;
category: typeof unicodeEmojiCategories[number]; category: typeof unicodeEmojiCategories[number];
}; }
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
import _emojilist from './emojilist.json' with { type: 'json' }; import _emojilist from './emojilist.json';
export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
name: x[1] as string, name: x[1] as string,

View File

@@ -2,7 +2,6 @@
* SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ILocale, ParameterizedString } from '../../../locales/index.js'; import type { ILocale, ParameterizedString } from '../../../locales/index.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -134,6 +134,7 @@ export function scrollToBottom(
export function isTopVisible(el: HTMLElement, tolerance = 1): boolean { export function isTopVisible(el: HTMLElement, tolerance = 1): boolean {
const scrollTop = getScrollPosition(el); const scrollTop = getScrollPosition(el);
if (_DEV_) console.log(scrollTop, tolerance, scrollTop <= tolerance);
return scrollTop <= tolerance; return scrollTop <= tolerance;
} }

View File

@@ -21,13 +21,13 @@
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "22.13.5", "@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "8.24.1", "@typescript-eslint/parser": "7.17.0",
"esbuild": "0.25.0", "esbuild": "0.24.0",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.31.0",
"nodemon": "3.1.9", "nodemon": "3.1.7",
"typescript": "5.7.3", "typescript": "5.6.3",
"vue-eslint-parser": "9.4.3" "vue-eslint-parser": "9.4.3"
}, },
"files": [ "files": [
@@ -35,6 +35,6 @@
], ],
"dependencies": { "dependencies": {
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"vue": "3.5.13" "vue": "3.5.12"
} }
} }

View File

@@ -414,7 +414,6 @@ function toStories(component: string): Promise<string> {
glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkSignupServerRules.vue'),
glob('src/components/MkUserSetupDialog.vue'), glob('src/components/MkUserSetupDialog.vue'),
glob('src/components/MkUserSetupDialog.*.vue'), glob('src/components/MkUserSetupDialog.*.vue'),
glob('src/components/MkImgPreviewDialog.vue'),
glob('src/components/MkInstanceCardMini.vue'), glob('src/components/MkInstanceCardMini.vue'),
glob('src/components/MkInviteCode.vue'), glob('src/components/MkInviteCode.vue'),
glob('src/components/MkTagItem.vue'), glob('src/components/MkTagItem.vue'),

View File

@@ -47,8 +47,6 @@ export default [
'@typescript-eslint/no-empty-interface': ['error', { '@typescript-eslint/no-empty-interface': ['error', {
allowSingleExtends: true, allowSingleExtends: true,
}], }],
// defineExposeが誤検知されてしまう
'@typescript-eslint/no-unused-expressions': 'off',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため

View File

@@ -16,34 +16,32 @@
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"dependencies": { "dependencies": {
"@analytics/google-analytics": "1.1.0",
"@discordapp/twemoji": "15.1.0", "@discordapp/twemoji": "15.1.0",
"@github/webauthn-json": "2.1.1", "@github/webauthn-json": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@misskey-dev/browser-image-resizer": "2024.1.0", "@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2", "@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.4", "@rollup/pluginutils": "5.1.3",
"@syuilo/aiscript": "0.19.0", "@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.30.0-mi.1932+ab127beee.tar.gz", "@tabler/icons-webfont": "https://github.com/misskey-dev/tabler-icons/archive/refs/tags/3.29.0-mi.1913+5921534bc.tar.gz",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.0",
"@vue/compiler-sfc": "3.5.13", "@vue/compiler-sfc": "3.5.12",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.16",
"astring": "1.9.0", "astring": "1.9.0",
"broadcast-channel": "7.0.0", "broadcast-channel": "7.0.0",
"buraha": "0.0.1", "buraha": "0.0.1",
"canvas-confetti": "1.9.3", "canvas-confetti": "1.9.3",
"chart.js": "4.4.8", "chart.js": "4.4.6",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.0.1", "chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0", "chartjs-plugin-zoom": "2.0.1",
"chromatic": "11.25.2", "chromatic": "11.18.1",
"compare-versions": "6.1.1", "compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.2", "cropperjs": "2.0.0-rc.2",
"date-fns": "4.1.0", "date-fns": "2.30.0",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"frontend-shared": "workspace:*", "frontend-shared": "workspace:*",
@@ -51,91 +49,92 @@
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2", "is-file-animated": "1.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"matter-js": "0.20.0", "matter-js": "0.19.0",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"misskey-bubble-game": "workspace:*", "misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"photoswipe": "5.4.4", "photoswipe": "5.4.4",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.34.8", "rollup": "4.26.0",
"sanitize-html": "2.14.0", "sanitize-html": "2.13.1",
"sass": "1.85.0", "sass": "1.79.3",
"shiki": "3.0.0", "shiki": "1.22.2",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.173.0", "three": "0.169.0",
"throttle-debounce": "5.0.2", "throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typescript": "5.7.3", "typescript": "5.6.3",
"uuid": "11.1.0", "uuid": "10.0.0",
"v-code-diff": "1.13.1", "v-code-diff": "1.13.1",
"vite": "6.1.1", "vite": "5.4.11",
"vue": "3.5.13", "vue": "3.5.12",
"vuedraggable": "next" "vuedraggable": "next"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/summaly": "5.2.0", "@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.5.8", "@storybook/addon-actions": "8.4.4",
"@storybook/addon-essentials": "8.5.8", "@storybook/addon-essentials": "8.4.4",
"@storybook/addon-interactions": "8.5.8", "@storybook/addon-interactions": "8.4.4",
"@storybook/addon-links": "8.5.8", "@storybook/addon-links": "8.4.4",
"@storybook/addon-mdx-gfm": "8.5.8", "@storybook/addon-mdx-gfm": "8.4.4",
"@storybook/addon-storysource": "8.5.8", "@storybook/addon-storysource": "8.4.4",
"@storybook/blocks": "8.5.8", "@storybook/blocks": "8.4.4",
"@storybook/components": "8.5.8", "@storybook/components": "8.4.4",
"@storybook/core-events": "8.5.8", "@storybook/core-events": "8.4.4",
"@storybook/manager-api": "8.5.8", "@storybook/manager-api": "8.4.4",
"@storybook/preview-api": "8.5.8", "@storybook/preview-api": "8.4.4",
"@storybook/react": "8.5.8", "@storybook/react": "8.4.4",
"@storybook/react-vite": "8.5.8", "@storybook/react-vite": "8.4.4",
"@storybook/test": "8.5.8", "@storybook/test": "8.4.4",
"@storybook/theming": "8.5.8", "@storybook/theming": "8.4.4",
"@storybook/types": "8.5.8", "@storybook/types": "8.4.4",
"@storybook/vue3": "8.5.8", "@storybook/vue3": "8.4.4",
"@storybook/vue3-vite": "8.5.8", "@storybook/vue3-vite": "8.4.4",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0", "@types/canvas-confetti": "^1.6.4",
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"@types/matter-js": "0.19.8", "@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
"@types/node": "22.13.5", "@types/node": "22.9.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.13.0", "@types/sanitize-html": "2.13.0",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.5.14", "@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.24.1", "@types/ws": "8.5.13",
"@typescript-eslint/parser": "8.24.1", "@typescript-eslint/eslint-plugin": "7.17.0",
"@vitest/coverage-v8": "3.0.6", "@typescript-eslint/parser": "7.17.0",
"@vue/runtime-core": "3.5.13", "@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.12",
"acorn": "8.14.0", "acorn": "8.14.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "14.0.3", "cypress": "13.15.2",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.31.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.2",
"happy-dom": "17.1.4", "happy-dom": "10.0.3",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"msw": "2.7.1", "msw": "2.6.4",
"msw-storybook-addon": "2.0.4", "msw-storybook-addon": "2.0.4",
"nodemon": "3.1.9", "nodemon": "3.1.7",
"prettier": "3.5.2", "prettier": "3.3.3",
"react": "19.0.0", "react": "18.3.1",
"react-dom": "19.0.0", "react-dom": "18.3.1",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"start-server-and-test": "2.0.10", "start-server-and-test": "2.0.8",
"storybook": "8.5.8", "storybook": "8.4.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "3.0.6", "vitest": "1.6.0",
"vitest-fetch-mock": "0.4.3", "vitest-fetch-mock": "0.2.2",
"vue-component-type-helpers": "2.2.4", "vue-component-type-helpers": "2.1.10",
"vue-eslint-parser": "9.4.3", "vue-eslint-parser": "9.4.3",
"vue-tsc": "2.2.4" "vue-tsc": "2.1.10"
} }
} }

Some files were not shown because too many files have changed in this diff Show More