Merge remote-tracking branch 'misskey-original/develop' into develop

# Conflicts:
#	package.json
#	packages/backend/src/core/QueueService.ts
#	packages/frontend/src/components/global/MkPageHeader.vue
#	packages/frontend/src/pages/admin/index.vue
#	packages/frontend/src/pages/clicker.vue
#	packages/frontend/src/pages/my-lists/index.vue
#	packages/frontend/src/pages/notifications.vue
#	packages/frontend/src/pages/settings/emoji-picker.vue
#	packages/frontend/src/pages/settings/mute-block.vue
#	packages/frontend/src/pages/timeline.vue
#	packages/frontend/src/ui/universal.vue
#	pnpm-lock.yaml
This commit is contained in:
mattyatea 2024-02-16 19:40:51 +09:00
commit 2b52a215e5
238 changed files with 3369 additions and 3002 deletions

View file

@ -20,7 +20,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

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

View file

@ -19,7 +19,7 @@ jobs:
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v4 uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
@ -31,7 +31,7 @@ jobs:
- name: setup node - name: setup node
id: setup-node id: setup-node
uses: actions/setup-node@v4 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: pnpm cache: pnpm
@ -48,7 +48,7 @@ jobs:
wait-interval: 30 wait-interval: 30
- name: Download artifact - name: Download artifact
uses: actions/github-script@v7 uses: actions/github-script@v7.0.1
with: with:
script: | script: |
const fs = require('fs'); const fs = require('fs');

View file

@ -23,16 +23,35 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/preview') if: github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/preview')
outputs: outputs:
is-allowed-user: ${{ steps.check-allowed-users.outputs.is-allowed-user }}
pr-ref: ${{ steps.get-ref.outputs.pr-ref }} pr-ref: ${{ steps.get-ref.outputs.pr-ref }}
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 uses: actions/checkout@v4.1.1
- name: Check allowed users
id: check-allowed-users
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_ID: ${{ github.repository_owner_id }}
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
run: |
MEMBERSHIP_STATUS=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/organizations/$ORG_ID/public_members/$COMMENT_AUTHOR" \
-o /dev/null -w '%{http_code}\n' -s)
if [ "$MEMBERSHIP_STATUS" -eq 204 ]; then
echo "is-allowed-user=true" > $GITHUB_OUTPUT
else
echo "is-allowed-user=false" > $GITHUB_OUTPUT
fi
- name: Get PR ref - name: Get PR ref
id: get-ref id: get-ref
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
PR_NUMBER=$(jq --raw-output .issue.number $GITHUB_EVENT_PATH) PR_NUMBER=$(jq --raw-output .issue.number $GITHUB_EVENT_PATH)
PR_REF=$(gh pr view $PR_NUMBER --json headRefName -q '.headRefName') PR_REF=$(gh pr view $PR_NUMBER --json headRefName -q '.headRefName')
@ -40,13 +59,15 @@ jobs:
- name: Extract wait time - name: Extract wait time
id: get-wait-time id: get-wait-time
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: | run: |
COMMENT_BODY="${{ github.event.comment.body }}"
WAIT_TIME=$(echo "$COMMENT_BODY" | grep -oP '(?<=/preview\s)\d+' || echo "1800") WAIT_TIME=$(echo "$COMMENT_BODY" | grep -oP '(?<=/preview\s)\d+' || echo "1800")
echo "wait_time=$WAIT_TIME" > $GITHUB_OUTPUT echo "wait_time=$WAIT_TIME" > $GITHUB_OUTPUT
deploy-test-environment-pr-comment: deploy-test-environment-pr-comment:
needs: get-pr-ref needs: get-pr-ref
if: needs.get-pr-ref.outputs.is-allowed-user == 'true'
uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main
with: with:
repository: ${{ github.repository }} repository: ${{ github.repository }}

View file

@ -6,38 +6,83 @@ on:
- develop - develop
workflow_dispatch: workflow_dispatch:
env:
REGISTRY_IMAGE: misskey/misskey
jobs: jobs:
push_to_registry: # see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
name: Push Docker image to Docker Hub build:
name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
if: github.repository == 'misskey-dev/misskey' if: github.repository == 'misskey-dev/misskey'
steps: steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v3.0.0
with:
platforms: linux/amd64,linux/arm64
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: misskey/misskey
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub - name: Build and push by digest
id: build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
builder: ${{ steps.buildx.outputs.name }}
context: . context: .
push: true push: true
platforms: ${{ steps.buildx.outputs.platforms }} platforms: ${{ matrix.platform }}
provenance: false provenance: false
tags: misskey/misskey:develop
labels: develop labels: develop
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create --tag ${{ env.REGISTRY_IMAGE }}:develop \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:develop

View file

@ -5,45 +5,101 @@ on:
types: [published] types: [published]
workflow_dispatch: workflow_dispatch:
jobs: env:
push_to_registry: REGISTRY_IMAGE: misskey/misskey
name: Push Docker image to Docker Hub TAGS: |
runs-on: ubuntu-latest type=edge
type=ref,event=pr
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
jobs:
# see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
build:
name: Build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps: steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v3.0.0
with:
platforms: linux/amd64,linux/arm64
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: misskey/misskey images: ${{ env.REGISTRY_IMAGE }}
tags: | tags: ${{ env.TAGS }}
type=edge
type=ref,event=pr
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub - name: Build and Push to Docker Hub
id: build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
builder: ${{ steps.buildx.outputs.name }}
context: . context: .
push: true push: true
platforms: ${{ steps.buildx.outputs.platforms }} platforms: ${{ matrix.platform }}
provenance: false provenance: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: ${{ env.TAGS }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}

View file

@ -37,7 +37,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -31,7 +31,7 @@ jobs:
with: with:
version: 8 version: 8
run_install: false run_install: false
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -58,7 +58,7 @@ jobs:
with: with:
version: 7 version: 7
run_install: false run_install: false
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -84,7 +84,7 @@ jobs:
with: with:
version: 7 version: 7
run_install: false run_install: false
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.2
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

@ -20,7 +20,7 @@ jobs:
node-version: [20.10.0] node-version: [20.10.0]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
- name: Install pnpm - name: Install pnpm
@ -29,7 +29,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -13,7 +13,7 @@ jobs:
github.event.client_payload.slash_command.sha != '' && github.event.client_payload.slash_command.sha != '' &&
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
steps: steps:
- uses: actions/github-script@v7 - uses: actions/github-script@v7.0.1
id: check-id id: check-id
env: env:
number: ${{ github.event.client_payload.pull_request.number }} number: ${{ github.event.client_payload.pull_request.number }}
@ -37,7 +37,7 @@ jobs:
return check[0].id; return check[0].id;
- uses: actions/github-script@v7 - uses: actions/github-script@v7.0.1
env: env:
check_id: ${{ steps.check-id.outputs.result }} check_id: ${{ steps.check-id.outputs.result }}
details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
@ -72,7 +72,7 @@ jobs:
timeout: 15m timeout: 15m
# Update check run called "integration-fork" # Update check run called "integration-fork"
- uses: actions/github-script@v7 - uses: actions/github-script@v7.0.1
id: update-check-run id: update-check-run
if: ${{ always() }} if: ${{ always() }}
env: env:

View file

@ -10,7 +10,7 @@ jobs:
destroy-preview-environment: destroy-preview-environment:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v7 - uses: actions/github-script@v7.0.1
id: check-conclusion id: check-conclusion
env: env:
number: ${{ github.event.number }} number: ${{ github.event.number }}

View file

@ -16,7 +16,7 @@ jobs:
# api-artifact # api-artifact
steps: steps:
- name: Download artifact - name: Download artifact
uses: actions/github-script@v7 uses: actions/github-script@v7.0.1
with: with:
script: | script: |
const fs = require('fs'); const fs = require('fs');

113
.github/workflows/storybook.yml vendored Normal file
View file

@ -0,0 +1,113 @@
name: Storybook
on:
push:
branches:
- master
- develop
- dev/storybook8 # for testing
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
env:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- uses: actions/checkout@v4.1.1
if: github.event_name != 'pull_request_target'
with:
fetch-depth: 0
submodules: true
- uses: actions/checkout@v4.1.1
if: github.event_name == 'pull_request_target'
with:
fetch-depth: 0
submodules: true
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Checkout actual HEAD
if: github.event_name == 'pull_request_target'
id: rev
run: |
echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT
git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3)
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Use Node.js 20.x
uses: actions/setup-node@v4.0.2
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build misskey-js
run: pnpm --filter misskey-js build
- name: Build storybook
run: pnpm --filter frontend build-storybook
- name: Publish to Chromatic
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/master'
run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Publish to Chromatic
if: github.event_name != 'pull_request_target' && github.ref != 'refs/heads/master'
id: chromatic_push
run: |
DIFF="${{ github.event.before }} HEAD"
if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then
DIFF="HEAD"
fi
CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))"
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
fi
if pnpm --filter frontend chromatic -d storybook-static $(echo "$CHROMATIC_PARAMETER"); then
echo "success=true" >> $GITHUB_OUTPUT
else
echo "success=false" >> $GITHUB_OUTPUT
fi
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Publish to Chromatic
if: github.event_name == 'pull_request_target'
id: chromatic_pull_request
run: |
DIFF="${{ steps.rev.outputs.base }} HEAD"
if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then
DIFF="HEAD"
fi
CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))"
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
fi
BRANCH="${{ github.event.pull_request.head.user.login }}:${{ github.event.pull_request.head.ref }}"
if [ "$BRANCH" = "misskey-dev:${{ github.event.pull_request.head.ref }}" ]; then
BRANCH="${{ github.event.pull_request.head.ref }}"
fi
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER")
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Notify that Chromatic detects changes
uses: actions/github-script@v7.0.1
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
})
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: storybook
path: packages/frontend/storybook-static

View file

@ -46,7 +46,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -96,7 +96,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -38,7 +38,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -96,7 +96,7 @@ jobs:
version: 7 version: 7
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -30,7 +30,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -28,7 +28,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -29,7 +29,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -19,11 +19,11 @@
### General ### General
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加 - Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
- Feat: Add support for TrueMail - Feat: Add support for TrueMail
- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
- Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正 - Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正
* すべてのリモートユーザーのリアクション一覧を見えないようにします * すべてのリモートユーザーのリアクション一覧を見えないようにします
- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように
- Fix: 特定のキーワード及び正規表現にマッチする文字列を含むノートが投稿された際、エラーに出来るような設定項目を追加 #13207 - Fix: 特定のキーワード及び正規表現にマッチする文字列を含むノートが投稿された際、エラーに出来るような設定項目を追加 #13207
* デフォルトは空欄なので適用前と同等の動作になります * デフォルトは空欄なので適用前と同等の動作になります
@ -57,6 +57,7 @@
- リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合 - リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合
- センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合 - センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合
- ロールが必要な絵文字をリアクションしようとした場合 - ロールが必要な絵文字をリアクションしようとした場合
- Enhance: ページ遷移時にPlayerを閉じるように
- Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: ネイティブモードの絵文字がモノクロにならないように
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
@ -67,7 +68,6 @@
- Fix: デッキのプロファイル作成時に名前を空にできる問題を修正 - Fix: デッキのプロファイル作成時に名前を空にできる問題を修正
- Fix: テーマ作成時に名称が空欄でも作成できてしまう問題を修正 - Fix: テーマ作成時に名称が空欄でも作成できてしまう問題を修正
- Fix: プラグインで`Plugin:register_note_post_interruptor`を使用すると、ノートが投稿できなくなる問題を修正 - Fix: プラグインで`Plugin:register_note_post_interruptor`を使用すると、ノートが投稿できなくなる問題を修正
- Enhance: ページ遷移時にPlayerを閉じるように
- Fix: iOSで大きな画像を変換してアップロードできない問題を修正 - Fix: iOSで大きな画像を変換してアップロードできない問題を修正
- Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正 - Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正
- Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正 - Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正
@ -79,11 +79,12 @@
- Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196 - Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196
### Server ### Server
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました - Enhance: 連合先のレートリミットを超過した際にリトライするようになりました
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) - Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
- Enhance: クリップをエクスポートできるように - Enhance: クリップをエクスポートできるように
- Enhance: `/files`のファイルに対してHTTP Rangeリクエストを行えるように - Enhance: `/files`のファイルに対してHTTP Rangeリクエストを行えるように
- Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新 - Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新
- Enhance: 連合向けのノート配信を軽量化 #13192
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正 - Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
- Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更 - Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更
- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更 - Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更
@ -91,7 +92,7 @@
- Fix: properly handle cc followers - Fix: properly handle cc followers
- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec - Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec
- Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122 - Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122
- Enhance: 連合向けのノート配信を軽量化 #13192 - Fix: リモートユーザーが復活してもキャッシュにより該当ユーザーのActivityが受け入れられないのを修正 #13273
### Service Worker ### Service Worker
- Enhance: オフライン表示のデザインを改善・多言語対応 - Enhance: オフライン表示のデザインを改善・多言語対応

View file

@ -27,13 +27,13 @@ COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
ARG NODE_ENV=production
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output pnpm i --frozen-lockfile --aggregate-output
COPY --link . ./ COPY --link . ./
ARG NODE_ENV=production
RUN git submodule update --init RUN git submodule update --init
RUN pnpm build RUN pnpm build
RUN rm -rf .git/ RUN rm -rf .git/
@ -57,6 +57,8 @@ COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
ARG NODE_ENV=production
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output pnpm i --frozen-lockfile --aggregate-output

View file

@ -1997,8 +1997,12 @@ _permissions:
"read:admin:abuse-user-reports": "View user reports" "read:admin:abuse-user-reports": "View user reports"
"write:admin:delete-account": "Delete user account" "write:admin:delete-account": "Delete user account"
"write:admin:delete-all-files-of-a-user": "Delete all files of a user" "write:admin:delete-all-files-of-a-user": "Delete all files of a user"
"read:admin:index-stats": "View database index stats"
"read:admin:table-stats": "View database table stats"
"read:admin:user-ips": "View user IP addresses"
"read:admin:meta": "View instance metadata" "read:admin:meta": "View instance metadata"
"write:admin:reset-password": "Reset user password" "write:admin:reset-password": "Reset user password"
"write:admin:resolve-abuse-user-report": "Resolve user report"
"write:admin:send-email": "Send email" "write:admin:send-email": "Send email"
"read:admin:server-info": "View server info" "read:admin:server-info": "View server info"
"read:admin:show-moderation-log": "View moderation log" "read:admin:show-moderation-log": "View moderation log"
@ -2019,6 +2023,26 @@ _permissions:
"write:admin:announcements": "Manage announcements" "write:admin:announcements": "Manage announcements"
"read:admin:announcements": "View announcements" "read:admin:announcements": "View announcements"
"write:admin:avatar-decorations": "Manage avatar decorations" "write:admin:avatar-decorations": "Manage avatar decorations"
"read:admin:avatar-decorations": "View avatar decorations"
"write:admin:federation": "Manage federation data"
"write:admin:account": "Manage user account"
"read:admin:account": "View user account"
"write:admin:emoji": "Manage emoji"
"read:admin:emoji": "View emoji"
"write:admin:queue": "Manage job queue"
"read:admin:queue": "View job queue info"
"write:admin:promo": "Manage promotion notes"
"write:admin:drive": "Manage user drive"
"read:admin:drive": "View user drive info"
"read:admin:stream": "Use WebSocket API for Admin"
"write:admin:ad": "Manage ads"
"read:admin:ad": "View ads"
"write:invite-codes": "Create invite codes"
"read:invite-codes": "Get invite codes"
"write:clip-favorite": "Manage favorited clips"
"read:clip-favorite": "View favorited clips"
"read:federation": "Get federation data"
"write:report-abuse": "Report violation"
_auth: _auth:
shareAccessTitle: "Granting application permissions" shareAccessTitle: "Granting application permissions"
shareAccess: "Would you like to authorize \"{name}\" to access this account?" shareAccess: "Would you like to authorize \"{name}\" to access this account?"

View file

@ -2,7 +2,7 @@
_lang_: "Français" _lang_: "Français"
headlineMisskey: "Réseau relié par des notes" headlineMisskey: "Réseau relié par des notes"
introMisskey: "Bienvenue ! Misskey est un service de microblogage décentralisé, libre et ouvert.\nÉcrivez des « notes » et partagez ce qui se passe à linstant présent, autour de vous avec les autres 📡\nLa fonction « réactions », vous permet également dajouter une réaction rapide aux notes des autres utilisateur·rice·s 👍\nExplorons un nouveau monde 🚀" introMisskey: "Bienvenue ! Misskey est un service de microblogage décentralisé, libre et ouvert.\nÉcrivez des « notes » et partagez ce qui se passe à linstant présent, autour de vous avec les autres 📡\nLa fonction « réactions », vous permet également dajouter une réaction rapide aux notes des autres utilisateur·rice·s 👍\nExplorons un nouveau monde 🚀"
poweredByMisskeyDescription: "{nom} est l'un des services propulsés par la plateforme ouverte <b>Misskey</b> (appelée \"instance Misskey\")." poweredByMisskeyDescription: "{name} est l'un des services propulsés par la plateforme ouverte <b>Misskey</b> (appelée \"instance Misskey\")."
monthAndDay: "{day}/{month}" monthAndDay: "{day}/{month}"
search: "Rechercher" search: "Rechercher"
notifications: "Notifications" notifications: "Notifications"
@ -1175,7 +1175,7 @@ _initialAccountSetting:
profileSetting: "Paramètres du profil" profileSetting: "Paramètres du profil"
privacySetting: "Paramètres de confidentialité" privacySetting: "Paramètres de confidentialité"
initialAccountSettingCompleted: "Configuration du profil terminée avec succès !" initialAccountSettingCompleted: "Configuration du profil terminée avec succès !"
youCanContinueTutorial: "Vous pouvez procéder au tutoriel sur l'utilisation de {nom}(Misskey) ou vous arrêter ici et commencer à l'utiliser immédiatement." youCanContinueTutorial: "Vous pouvez procéder au tutoriel sur l'utilisation de {name}(Misskey) ou vous arrêter ici et commencer à l'utiliser immédiatement."
startTutorial: "Démarrer le tutoriel" startTutorial: "Démarrer le tutoriel"
skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?" skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?"
_initialTutorial: _initialTutorial:

View file

@ -1041,6 +1041,8 @@ resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
sensitiveWords: "민감한 단어" sensitiveWords: "민감한 단어"
sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다." sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다."
sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
prohibitedWords: "금지 워드"
prohibitedWordsDescription: "설정된 워드가 포함되는 노트를 작성하려고 하면, 에러가 발생하도록 합니다. 줄바꿈으로 구분지어 복수 설정할 수 있습니다."
prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
hiddenTags: "숨긴 해시태그" hiddenTags: "숨긴 해시태그"
hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다." hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다."

View file

@ -1041,6 +1041,8 @@ resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุ
sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน" sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน"
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ" sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ" sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
prohibitedWords: "คำต้องห้าม"
prohibitedWordsDescription: "จะแจ้งเตือนว่าเกิดข้อผิดพลาดเมื่อพยายามโพสต์โน้ตที่มีคำที่กำหนดไว้ สามารถตั้งได้หลายคำด้วยการขึ้นบรรทัดใหม่"
prohibitedWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ" prohibitedWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
hiddenTags: "แฮชแท็กที่ซ่อนอยู่" hiddenTags: "แฮชแท็กที่ซ่อนอยู่"
hiddenTagsDescription: "เลือกแท็กที่จะไม่แสดงในรายการเทรนด์ สามารถลงทะเบียนหลายแท็กได้โดยขึ้นบรรทัดใหม่" hiddenTagsDescription: "เลือกแท็กที่จะไม่แสดงในรายการเทรนด์ สามารถลงทะเบียนหลายแท็กได้โดยขึ้นบรรทัดใหม่"
@ -1203,6 +1205,9 @@ replaying: "กำลังรีเพลย์"
ranking: "อันดับ" ranking: "อันดับ"
lastNDays: "ล่าสุด {n} วันที่แล้ว" lastNDays: "ล่าสุด {n} วันที่แล้ว"
backToTitle: "กลับไปหน้าไตเติ้ล" backToTitle: "กลับไปหน้าไตเติ้ล"
hemisphere: "พื้นที่ที่อาศัยอยู่"
withSensitive: "แสดงโน้ตที่มีไฟล์ที่ระบุว่ามีเนื้อหาละเอียดอ่อน"
userSaysSomethingSensitive: "โพสต์ที่มีไฟล์เนื้อหาละเอียดอ่อนของ {name}"
enableHorizontalSwipe: "ปัดเพื่อสลับแท็บ" enableHorizontalSwipe: "ปัดเพื่อสลับแท็บ"
_bubbleGame: _bubbleGame:
howToPlay: "วิธีเล่น" howToPlay: "วิธีเล่น"
@ -2439,6 +2444,53 @@ _dataSaver:
_code: _code:
title: "ไฮไลต์โค้ด" title: "ไฮไลต์โค้ด"
description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้" description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้"
_hemisphere:
N: "ซีกโลกเหนือ"
S: "ซีกโลกใต้"
caption: "ใช้เพื่อกำหนดฤดูกาลของไคลเอ็นต์"
_reversi: _reversi:
reversi: "รีเวอร์ซี"
gameSettings: "ตั้งค่าการเล่น"
chooseBoard: "เลือกกระดาน"
blackOrWhite: "ดำ/ขาว"
blackIs: "{name}เป็นสีดำ"
rules: "กฎ"
thisGameIsStartedSoon: "การเล่นจะเริ่มแล้ว"
waitingForOther: "กำลังรออีกฝ่ายเตรียมตัวให้เสร็จ"
waitingForMe: "กำลังรอฝ่ายคุณเตรียมตัวให้เสร็จ"
waitingBoth: "กรุณาเตรียมตัว"
ready: "เตรียมตัวพร้อมแล้ว"
cancelReady: "ยกเลิกการเตรียมตัวพร้อม"
opponentTurn: "ตาอีกฝ่าย"
myTurn: "ตาของคุณ"
turnOf: "ตาของ{name}"
pastTurnOf: "ตาของ{name}"
surrender: "ยอมแพ้"
surrendered: "ยอมแพ้แล้ว"
timeout: "หมดเวลาแล้ว"
drawn: "เสมอ"
won: "{name}ชนะ"
black: "ดำ"
white: "ขาว"
total: "รวมทั้งหมด" total: "รวมทั้งหมด"
turnCount: "ตาที่{count}"
myGames: "การเล่นของตัวเอง"
allGames: "การเล่นของทุกคน"
ended: "จบ"
playing: "กำลังเล่น"
isLlotheo: "คนที่มีตัวหมากน้อยกว่าชนะ (Roseo)"
loopedMap: "ลูปแมป"
canPutEverywhere: "โหมดที่สามารถวางได้ทุกที่"
timeLimitForEachTurn: "จำกัดเวลาต่อแต่ละตา"
freeMatch: "ฟรีแมตช์"
lookingForPlayer: "กำลังมองหาคู่ต่อสู้อยู่"
gameCanceled: "ยกเลิกการเล่นแล้ว"
shareToTlTheGameWhenStart: "โพสต์ลงไทม์ไลน์เมื่อเริ่มการเล่น"
iStartedAGame: "เริ่มเล่นหมากรีเวอร์ซีแล้ว! #MisskeyReversi"
opponentHasSettingsChanged: "อีกฝ่ายเปลี่ยนการตั้งค่า"
allowIrregularRules: "อนุญาตกฎที่ไม่ปรกติ (โหมดฟรีทุกอย่าง)"
disallowIrregularRules: "ไม่อนุญาตกฎที่ไม่ปรกติ"
_offlineScreen:
title: "ออฟไลน์ - ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้"
header: "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้"

View file

@ -11,7 +11,7 @@ password: "密码"
forgotPassword: "忘记密码" forgotPassword: "忘记密码"
fetchingAsApObject: "在联邦宇宙查询中..." fetchingAsApObject: "在联邦宇宙查询中..."
ok: "OK" ok: "OK"
gotIt: "我明白了" gotIt: ""
cancel: "取消" cancel: "取消"
noThankYou: "不用,谢谢" noThankYou: "不用,谢谢"
enterUsername: "输入用户名" enterUsername: "输入用户名"
@ -1041,6 +1041,7 @@ resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词" sensitiveWords: "敏感词"
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。" sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
prohibitedWords: "禁用词"
prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
hiddenTags: "隐藏标签" hiddenTags: "隐藏标签"
hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。" hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。"

View file

@ -66,7 +66,7 @@ showMore: "載入更多"
showLess: "關閉" showLess: "關閉"
youGotNewFollower: "您有新的追隨者" youGotNewFollower: "您有新的追隨者"
receiveFollowRequest: "您有新的追隨請求" receiveFollowRequest: "您有新的追隨請求"
followRequestAccepted: "追隨請求已接受" followRequestAccepted: "追隨請求已接受"
mention: "提及" mention: "提及"
mentions: "提及" mentions: "提及"
directNotes: "私訊" directNotes: "私訊"
@ -604,7 +604,7 @@ inboxUrl: "收件夾URL"
addedRelays: "已加入的中繼器" addedRelays: "已加入的中繼器"
serviceworkerInfo: "如要使用推播通知,需要啟用此選項並設定金鑰。" serviceworkerInfo: "如要使用推播通知,需要啟用此選項並設定金鑰。"
deletedNote: "已刪除的貼文" deletedNote: "已刪除的貼文"
invisibleNote: "私密的貼文" invisibleNote: "私貼文"
enableInfiniteScroll: "啟用自動滾動頁面模式" enableInfiniteScroll: "啟用自動滾動頁面模式"
visibility: "可見性" visibility: "可見性"
poll: "票選活動" poll: "票選活動"
@ -1195,7 +1195,7 @@ overwriteContentConfirm: "確定要覆蓋目前的內容嗎?"
seasonalScreenEffect: "隨季節變換畫面的呈現" seasonalScreenEffect: "隨季節變換畫面的呈現"
decorate: "設置頭像裝飾" decorate: "設置頭像裝飾"
addMfmFunction: "插入MFM功能語法" addMfmFunction: "插入MFM功能語法"
enableQuickAddMfmFunction: "顯示高級MFM選擇器" enableQuickAddMfmFunction: "顯示高級 MFM 選擇器"
bubbleGame: "氣泡遊戲" bubbleGame: "氣泡遊戲"
sfx: "音效" sfx: "音效"
soundWillBePlayed: "將播放音效" soundWillBePlayed: "將播放音效"
@ -2096,7 +2096,7 @@ _poll:
deadlineTime: "小時" deadlineTime: "小時"
duration: "時長" duration: "時長"
votesCount: "{n} 票" votesCount: "{n} 票"
totalVotes: "一共{n}票" totalVotes: "合計 {n} 票"
vote: "投票" vote: "投票"
showResult: "顯示結果" showResult: "顯示結果"
voted: "已投票" voted: "已投票"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.2.0-beta.11-PrisMisskey.1", "version": "2024.2.0-beta.12-PrisMisskey.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -16,10 +16,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
export class CacheService implements OnApplicationShutdown { export class CacheService implements OnApplicationShutdown {
public userByIdCache: MemoryKVCache<MiUser, MiUser | string>; public userByIdCache: MemoryKVCache<MiUser>;
public localUserByNativeTokenCache: MemoryKVCache<MiLocalUser | null, string | null>; public localUserByNativeTokenCache: MemoryKVCache<MiLocalUser | null>;
public localUserByIdCache: MemoryKVCache<MiLocalUser>; public localUserByIdCache: MemoryKVCache<MiLocalUser>;
public uriPersonCache: MemoryKVCache<MiUser | null, string | null>; public uriPersonCache: MemoryKVCache<MiUser | null>;
public userProfileCache: RedisKVCache<MiUserProfile>; public userProfileCache: RedisKVCache<MiUserProfile>;
public userMutingsCache: RedisKVCache<Set<string>>; public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>; public userBlockingCache: RedisKVCache<Set<string>>;
@ -56,41 +56,10 @@ export class CacheService implements OnApplicationShutdown {
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
const localUserByIdCache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 60 * 6 /* 6h */); this.userByIdCache = new MemoryKVCache<MiUser>(Infinity);
this.localUserByIdCache = localUserByIdCache; this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity);
this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity);
// ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity);
const userByIdCache = new MemoryKVCache<MiUser, MiUser | string>(1000 * 60 * 60 * 6 /* 6h */, {
toMapConverter: user => {
if (user.host === null) {
localUserByIdCache.set(user.id, user as MiLocalUser);
return user.id;
}
return user;
},
fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId,
});
this.userByIdCache = userByIdCache;
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null, string | null>(Infinity, {
toMapConverter: user => {
if (user === null) return null;
localUserByIdCache.set(user.id, user);
return user.id;
},
fromMapConverter: id => id === null ? null : localUserByIdCache.get(id),
});
this.uriPersonCache = new MemoryKVCache<MiUser | null, string | null>(Infinity, {
toMapConverter: user => {
if (user === null) return null;
userByIdCache.set(user.id, user);
return user.id;
},
fromMapConverter: id => id === null ? null : userByIdCache.get(id),
});
this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', { this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', {
lifetime: 1000 * 60 * 30, // 30m lifetime: 1000 * 60 * 30, // 30m
@ -160,16 +129,25 @@ export class CacheService implements OnApplicationShutdown {
switch (type) { switch (type) {
case 'userChangeSuspendedState': case 'userChangeSuspendedState':
case 'remoteUserUpdated': { case 'remoteUserUpdated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id }); const user = await this.usersRepository.findOneBy({ id: body.id });
this.userByIdCache.set(user.id, user); if (user == null) {
for (const [k, v] of this.uriPersonCache.cache.entries()) { this.userByIdCache.delete(body.id);
if (v.value === user.id) { for (const [k, v] of this.uriPersonCache.cache.entries()) {
this.uriPersonCache.set(k, user); if (v.value?.id === body.id) {
this.uriPersonCache.delete(k);
}
}
} else {
this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user);
}
}
if (this.userEntityService.isLocalUser(user)) {
this.localUserByNativeTokenCache.set(user.token!, user);
this.localUserByIdCache.set(user.id, user);
} }
}
if (this.userEntityService.isLocalUser(user)) {
this.localUserByNativeTokenCache.set(user.token!, user);
this.localUserByIdCache.set(user.id, user);
} }
break; break;
} }

View file

@ -14,9 +14,9 @@ export type FanoutTimelineName =
| `homeTimeline:${string}` | `homeTimeline:${string}`
| `homeTimelineWithFiles:${string}` // only notes with files are included | `homeTimelineWithFiles:${string}` // only notes with files are included
// local timeline // local timeline
| 'localTimeline' // replies are not included | `localTimeline` // replies are not included
| 'localTimelineWithFiles' // only non-reply notes with files are included | `localTimelineWithFiles` // only non-reply notes with files are included
| 'localTimelineWithReplies' // only replies are included | `localTimelineWithReplies` // only replies are included
| `localTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id. | `localTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id.
// antenna // antenna

View file

@ -12,11 +12,11 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue, ScheduleNotePostQueue } from './QueueModule.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue, ScheduleNotePostQueue } from './QueueModule.js';
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature'; import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
@Injectable() @Injectable()
export class QueueService { export class QueueService {

View file

@ -30,12 +30,12 @@ import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackPromise } from '@/misc/promise-tracker.js';
const FALLBACK = ''; const FALLBACK = '\u2764';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
const legacies: Record<string, string> = { const legacies: Record<string, string> = {
'like': '👍', 'like': '👍',
'love': '❤', // ここに記述する場合は異体字セレクタを入れない 'love': '\u2764', // ハート、異体字セレクタを入れない
'laugh': '😆', 'laugh': '😆',
'hmm': '🤔', 'hmm': '🤔',
'surprise': '😮', 'surprise': '😮',
@ -120,7 +120,7 @@ export class ReactionService {
let reaction = _reaction ?? FALLBACK; let reaction = _reaction ?? FALLBACK;
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
reaction = '❤️'; reaction = '\u2764';
} else if (_reaction) { } else if (_reaction) {
const custom = reaction.match(isCustomEmojiRegexp); const custom = reaction.match(isCustomEmojiRegexp);
if (custom) { if (custom) {

View file

@ -106,12 +106,12 @@ export class ApDbResolverService implements OnApplicationShutdown {
return await this.cacheService.userByIdCache.fetchMaybe( return await this.cacheService.userByIdCache.fetchMaybe(
parsed.id, parsed.id,
() => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined), () => this.usersRepository.findOneBy({ id: parsed.id, isDeleted: false }).then(x => x ?? undefined),
) as MiLocalUser | undefined ?? null; ) as MiLocalUser | undefined ?? null;
} else { } else {
return await this.cacheService.uriPersonCache.fetch( return await this.cacheService.uriPersonCache.fetch(
parsed.uri, parsed.uri,
() => this.usersRepository.findOneBy({ uri: parsed.uri }), () => this.usersRepository.findOneBy({ uri: parsed.uri, isDeleted: false }),
) as MiRemoteUser | null; ) as MiRemoteUser | null;
} }
} }
@ -136,8 +136,12 @@ export class ApDbResolverService implements OnApplicationShutdown {
if (key == null) return null; if (key == null) return null;
const user = await this.cacheService.findUserById(key.userId).catch(() => null) as MiRemoteUser | null;
if (user == null) return null;
if (user.isDeleted) return null;
return { return {
user: await this.cacheService.findUserById(key.userId) as MiRemoteUser, user,
key, key,
}; };
} }
@ -151,6 +155,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
key: MiUserPublickey | null; key: MiUserPublickey | null;
} | null> { } | null> {
const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser;
if (user.isDeleted) return null;
const key = await this.publicKeyByUserIdCache.fetch( const key = await this.publicKeyByUserIdCache.fetch(
user.id, user.id,

View file

@ -36,6 +36,8 @@ import { ApResolverService } from './ApResolverService.js';
import { ApAudienceService } from './ApAudienceService.js'; import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js'; import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js'; import { ApQuestionService } from './models/ApQuestionService.js';
import { CacheService } from '@/core/CacheService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Resolver } from './ApResolverService.js'; import type { Resolver } from './ApResolverService.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js'; import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
@ -84,6 +86,8 @@ export class ApInboxService {
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
private apQuestionService: ApQuestionService, private apQuestionService: ApQuestionService,
private queueService: QueueService, private queueService: QueueService,
private cacheService: CacheService,
private globalEventService: GlobalEventService,
) { ) {
this.logger = this.apLoggerService.logger; this.logger = this.apLoggerService.logger;
} }
@ -481,6 +485,8 @@ export class ApInboxService {
isDeleted: true, isDeleted: true,
}); });
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
return `ok: queued ${job.name} ${job.id}`; return `ok: queued ${job.name} ${job.id}`;
} }

View file

@ -31,7 +31,7 @@ export class ApMfmService {
const parsed = mfm.parse(srcMfm); const parsed = mfm.parse(srcMfm);
if (!apAppend && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) { if (!apAppend && parsed?.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
noMisskeyContent = true; noMisskeyContent = true;
} }

View file

@ -186,28 +186,14 @@ export class RedisSingleCache<T> {
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
function nothingToDo<T, V = T>(value: T): V { export class MemoryKVCache<T> {
return value as unknown as V; public cache: Map<string, { date: number; value: T; }>;
}
export class MemoryKVCache<T, V = T> {
public cache: Map<string, { date: number; value: V; }>;
private lifetime: number; private lifetime: number;
private gcIntervalHandle: NodeJS.Timeout; private gcIntervalHandle: NodeJS.Timeout;
private toMapConverter: (value: T) => V;
private fromMapConverter: (cached: V) => T | undefined;
constructor(lifetime: MemoryKVCache<never>['lifetime'], options: { constructor(lifetime: MemoryKVCache<never>['lifetime']) {
toMapConverter: (value: T) => V;
fromMapConverter: (cached: V) => T | undefined;
} = {
toMapConverter: nothingToDo,
fromMapConverter: nothingToDo,
}) {
this.cache = new Map(); this.cache = new Map();
this.lifetime = lifetime; this.lifetime = lifetime;
this.toMapConverter = options.toMapConverter;
this.fromMapConverter = options.fromMapConverter;
this.gcIntervalHandle = setInterval(() => { this.gcIntervalHandle = setInterval(() => {
this.gc(); this.gc();
@ -218,7 +204,7 @@ export class MemoryKVCache<T, V = T> {
public set(key: string, value: T): void { public set(key: string, value: T): void {
this.cache.set(key, { this.cache.set(key, {
date: Date.now(), date: Date.now(),
value: this.toMapConverter(value), value,
}); });
} }
@ -230,7 +216,7 @@ export class MemoryKVCache<T, V = T> {
this.cache.delete(key); this.cache.delete(key);
return undefined; return undefined;
} }
return this.fromMapConverter(cached.value); return cached.value;
} }
@bindThis @bindThis
@ -241,10 +227,9 @@ export class MemoryKVCache<T, V = T> {
/** /**
* fetcherを呼び出して結果をキャッシュ& * fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
* fetcherの引数はcacheに保存されている値があれば渡されます
*/ */
@bindThis @bindThis
public async fetch(key: string, fetcher: (value: V | undefined) => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> { public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
const cachedValue = this.get(key); const cachedValue = this.get(key);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
if (validator) { if (validator) {
@ -259,7 +244,7 @@ export class MemoryKVCache<T, V = T> {
} }
// Cache MISS // Cache MISS
const value = await fetcher(this.cache.get(key)?.value); const value = await fetcher();
this.set(key, value); this.set(key, value);
return value; return value;
} }
@ -267,10 +252,9 @@ export class MemoryKVCache<T, V = T> {
/** /**
* fetcherを呼び出して結果をキャッシュ& * fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
* fetcherの引数はcacheに保存されている値があれば渡されます
*/ */
@bindThis @bindThis
public async fetchMaybe(key: string, fetcher: (value: V | undefined) => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> { public async fetchMaybe(key: string, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
const cachedValue = this.get(key); const cachedValue = this.get(key);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
if (validator) { if (validator) {
@ -285,7 +269,7 @@ export class MemoryKVCache<T, V = T> {
} }
// Cache MISS // Cache MISS
const value = await fetcher(this.cache.get(key)?.value); const value = await fetcher();
if (value !== undefined) { if (value !== undefined) {
this.set(key, value); this.set(key, value);
} }

View file

@ -0,0 +1,9 @@
import type { onRequestHookHandler } from 'fastify';
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
const index = request.url.indexOf('?');
if (~index) {
reply.redirect(301, request.url.slice(0, index));
}
done();
};

View file

@ -37,7 +37,17 @@ import { packedEmojiDetailedSchema, packedEmojiRequestSimpleSchema, packedEmojiS
import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js';
import { packedRoleLiteSchema, packedRoleSchema, packedRolePoliciesSchema } from '@/models/json-schema/role.js'; import {
packedRoleLiteSchema,
packedRoleSchema,
packedRolePoliciesSchema,
packedRoleCondFormulaLogicsSchema,
packedRoleCondFormulaValueNot,
packedRoleCondFormulaValueIsLocalOrRemoteSchema,
packedRoleCondFormulaValueCreatedSchema,
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
packedRoleCondFormulaValueSchema,
} from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js'; import { packedAdSchema } from '@/models/json-schema/ad.js';
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js'; import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
@ -80,6 +90,12 @@ export const refs = {
EmojiRequestDetailed: packedEmojiRequestDetailedSchema, EmojiRequestDetailed: packedEmojiRequestDetailedSchema,
Flash: packedFlashSchema, Flash: packedFlashSchema,
Signin: packedSigninSchema, Signin: packedSigninSchema,
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
RoleCondFormulaValue: packedRoleCondFormulaValueSchema,
RoleLite: packedRoleLiteSchema, RoleLite: packedRoleLiteSchema,
Role: packedRoleSchema, Role: packedRoleSchema,
RolePolicies: packedRolePoliciesSchema, RolePolicies: packedRolePoliciesSchema,

View file

@ -48,7 +48,7 @@ export class MiBubbleGameRecord {
@Column('jsonb', { @Column('jsonb', {
default: [], default: [],
}) })
public logs: any[]; public logs: number[][];
@Column('boolean', { @Column('boolean', {
default: false, default: false,

View file

@ -69,7 +69,7 @@ type CondFormulaValueNotesMoreThanOrEq = {
value: number; value: number;
}; };
export type RoleCondFormulaValue = export type RoleCondFormulaValue = { id: string } & (
CondFormulaValueAnd | CondFormulaValueAnd |
CondFormulaValueOr | CondFormulaValueOr |
CondFormulaValueNot | CondFormulaValueNot |
@ -82,7 +82,8 @@ export type RoleCondFormulaValue =
CondFormulaValueFollowingLessThanOrEq | CondFormulaValueFollowingLessThanOrEq |
CondFormulaValueFollowingMoreThanOrEq | CondFormulaValueFollowingMoreThanOrEq |
CondFormulaValueNotesLessThanOrEq | CondFormulaValueNotesLessThanOrEq |
CondFormulaValueNotesMoreThanOrEq; CondFormulaValueNotesMoreThanOrEq
);
@Entity('role') @Entity('role')
export class MiRole { export class MiRole {

View file

@ -47,12 +47,12 @@ export const packedReversiGameLiteSchema = {
user1: { user1: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'User', ref: 'UserLite',
}, },
user2: { user2: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'User', ref: 'UserLite',
}, },
winnerId: { winnerId: {
type: 'string', type: 'string',
@ -62,7 +62,7 @@ export const packedReversiGameLiteSchema = {
winner: { winner: {
type: 'object', type: 'object',
optional: false, nullable: true, optional: false, nullable: true,
ref: 'User', ref: 'UserLite',
}, },
surrenderedUserId: { surrenderedUserId: {
type: 'string', type: 'string',
@ -165,12 +165,12 @@ export const packedReversiGameDetailedSchema = {
user1: { user1: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'User', ref: 'UserLite',
}, },
user2: { user2: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'User', ref: 'UserLite',
}, },
winnerId: { winnerId: {
type: 'string', type: 'string',
@ -180,7 +180,7 @@ export const packedReversiGameDetailedSchema = {
winner: { winner: {
type: 'object', type: 'object',
optional: false, nullable: true, optional: false, nullable: true,
ref: 'User', ref: 'UserLite',
}, },
surrenderedUserId: { surrenderedUserId: {
type: 'string', type: 'string',
@ -226,6 +226,9 @@ export const packedReversiGameDetailedSchema = {
items: { items: {
type: 'array', type: 'array',
optional: false, nullable: false, optional: false, nullable: false,
items: {
type: 'number',
},
}, },
}, },
map: { map: {

View file

@ -1,3 +1,129 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedRoleCondFormulaLogicsSchema = {
type: 'object',
properties: {
id: {
type: 'string', optional: false,
},
type: {
type: 'string',
nullable: false, optional: false,
enum: ['and', 'or'],
},
values: {
type: 'array',
nullable: false, optional: false,
items: {
ref: 'RoleCondFormulaValue',
},
},
},
} as const;
export const packedRoleCondFormulaValueNot = {
type: 'object',
properties: {
id: {
type: 'string', optional: false,
},
type: {
type: 'string',
nullable: false, optional: false,
enum: ['not'],
},
value: {
type: 'object',
optional: false,
ref: 'RoleCondFormulaValue',
},
},
} as const;
export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = {
type: 'object',
properties: {
id: {
type: 'string', optional: false,
},
type: {
type: 'string',
nullable: false, optional: false,
enum: ['isLocal', 'isRemote'],
},
},
} as const;
export const packedRoleCondFormulaValueCreatedSchema = {
type: 'object',
properties: {
id: {
type: 'string', optional: false,
},
type: {
type: 'string',
nullable: false, optional: false,
enum: [
'createdLessThan',
'createdMoreThan',
],
},
sec: {
type: 'number',
nullable: false, optional: false,
},
},
} as const;
export const packedRoleCondFormulaFollowersOrFollowingOrNotesSchema = {
type: 'object',
properties: {
id: {
type: 'string', optional: false,
},
type: {
type: 'string',
nullable: false, optional: false,
enum: [
'followersLessThanOrEq',
'followersMoreThanOrEq',
'followingLessThanOrEq',
'followingMoreThanOrEq',
'notesLessThanOrEq',
'notesMoreThanOrEq',
],
},
value: {
type: 'number',
nullable: false, optional: false,
},
},
} as const;
export const packedRoleCondFormulaValueSchema = {
type: 'object',
oneOf: [
{
ref: 'RoleCondFormulaLogics',
},
{
ref: 'RoleCondFormulaValueNot',
},
{
ref: 'RoleCondFormulaValueIsLocalOrRemote',
},
{
ref: 'RoleCondFormulaValueCreated',
},
{
ref: 'RoleCondFormulaFollowersOrFollowingOrNotes',
},
],
} as const;
export const packedRolePoliciesSchema = { export const packedRolePoliciesSchema = {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
@ -174,6 +300,7 @@ export const packedRoleSchema = {
condFormula: { condFormula: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'RoleCondFormulaValue',
}, },
isPublic: { isPublic: {
type: 'boolean', type: 'boolean',

View file

@ -3,16 +3,38 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
const notificationRecieveConfig = { export const notificationRecieveConfig = {
type: 'object', type: 'object',
nullable: false, optional: true, oneOf: [
properties: { {
type: { type: 'object',
type: 'string', nullable: false,
nullable: false, optional: false, properties: {
enum: ['all', 'following', 'follower', 'mutualFollow', 'list', 'never'], type: {
type: 'string',
nullable: false,
enum: ['all', 'following', 'follower', 'mutualFollow', 'never'],
},
},
required: ['type'],
}, },
}, {
type: 'object',
nullable: false,
properties: {
type: {
type: 'string',
nullable: false,
enum: ['list'],
},
userListId: {
type: 'string',
format: 'misskey:id',
},
},
required: ['type', 'userListId'],
},
],
} as const; } as const;
export const packedUserLiteSchema = { export const packedUserLiteSchema = {
@ -555,15 +577,20 @@ export const packedMeDetailedOnlySchema = {
type: 'object', type: 'object',
nullable: false, optional: false, nullable: false, optional: false,
properties: { properties: {
app: notificationRecieveConfig, note: { optional: true, ...notificationRecieveConfig },
quote: notificationRecieveConfig, follow: { optional: true, ...notificationRecieveConfig },
reply: notificationRecieveConfig, mention: { optional: true, ...notificationRecieveConfig },
follow: notificationRecieveConfig, reply: { optional: true, ...notificationRecieveConfig },
renote: notificationRecieveConfig, renote: { optional: true, ...notificationRecieveConfig },
mention: notificationRecieveConfig, quote: { optional: true, ...notificationRecieveConfig },
reaction: notificationRecieveConfig, reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: notificationRecieveConfig, pollEnded: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: notificationRecieveConfig, receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },
achievementEarned: { optional: true, ...notificationRecieveConfig },
app: { optional: true, ...notificationRecieveConfig },
test: { optional: true, ...notificationRecieveConfig },
}, },
}, },
emailNotificationTypes: { emailNotificationTypes: {

View file

@ -34,7 +34,7 @@ export class RelationshipProcessorService {
@bindThis @bindThis
public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> { public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> {
this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? 'with replies' : 'without replies'}`); this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? "with replies" : "without replies"}`);
await this.userFollowingService.follow(job.data.from, job.data.to, { await this.userFollowingService.follow(job.data.from, job.data.to, {
requestId: job.data.requestId, requestId: job.data.requestId,
silent: job.data.silent, silent: job.data.silent,

View file

@ -27,6 +27,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
import { correctFilename } from '@/misc/correct-filename.js'; import { correctFilename } from '@/misc/correct-filename.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
@ -67,20 +68,23 @@ export class FileServerService {
done(); done();
}); });
fastify.get('/files/app-default.jpg', (request, reply) => { fastify.register((fastify, options, done) => {
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
reply.header('Content-Type', 'image/jpeg'); fastify.get('/files/app-default.jpg', (request, reply) => {
reply.header('Cache-Control', 'max-age=31536000, immutable'); const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
return reply.send(file); reply.header('Content-Type', 'image/jpeg');
}); reply.header('Cache-Control', 'max-age=31536000, immutable');
return reply.send(file);
});
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
return await this.sendDriveFile(request, reply) return await this.sendDriveFile(request, reply)
.catch(err => this.errorHandler(request, reply, err)); .catch(err => this.errorHandler(request, reply, err));
}); });
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
return await this.sendDriveFile(request, reply) return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`);
.catch(err => this.errorHandler(request, reply, err)); });
done();
}); });
fastify.get<{ fastify.get<{

View file

@ -37,12 +37,12 @@ export class NodeinfoServerService {
@bindThis @bindThis
public getLinks() { public getLinks() {
return [{ return [{
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
href: this.config.url + nodeinfo2_1path, href: this.config.url + nodeinfo2_1path
}, { }, {
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
href: this.config.url + nodeinfo2_0path, href: this.config.url + nodeinfo2_0path,
}]; }];
} }
@bindThis @bindThis

View file

@ -15,9 +15,6 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireAdmin: true, requireAdmin: true,
kind: 'write:admin:delete-account', kind: 'write:admin:delete-account',
res: {
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -84,6 +84,24 @@ export const meta = {
properties: { properties: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
properties: {
width: {
type: 'number',
optional: true, nullable: false,
},
height: {
type: 'number',
optional: true, nullable: false,
},
orientation: {
type: 'number',
optional: true, nullable: false,
},
avgColor: {
type: 'string',
optional: true, nullable: false,
},
},
}, },
storedInternal: { storedInternal: {
type: 'boolean', type: 'boolean',

View file

@ -18,6 +18,18 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
additionalProperties: {
type: 'object',
properties: {
count: {
type: 'number',
},
size: {
type: 'number',
},
},
required: ['count', 'size'],
},
example: { example: {
migrations: { migrations: {
count: 66, count: 66,

View file

@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -21,6 +22,157 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
nullable: false, optional: false, nullable: false, optional: false,
properties: {
email: {
type: 'string',
optional: false, nullable: true,
},
emailVerified: {
type: 'boolean',
optional: false, nullable: false,
},
autoAcceptFollowed: {
type: 'boolean',
optional: false, nullable: false,
},
noCrawle: {
type: 'boolean',
optional: false, nullable: false,
},
preventAiLearning: {
type: 'boolean',
optional: false, nullable: false,
},
alwaysMarkNsfw: {
type: 'boolean',
optional: false, nullable: false,
},
autoSensitive: {
type: 'boolean',
optional: false, nullable: false,
},
carefulBot: {
type: 'boolean',
optional: false, nullable: false,
},
injectFeaturedNote: {
type: 'boolean',
optional: false, nullable: false,
},
receiveAnnouncementEmail: {
type: 'boolean',
optional: false, nullable: false,
},
mutedWords: {
type: 'array',
optional: false, nullable: false,
items: {
anyOf: [
{
type: 'string',
},
{
type: 'array',
items: {
type: 'string',
},
},
],
},
},
mutedInstances: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
notificationRecieveConfig: {
type: 'object',
optional: false, nullable: false,
properties: {
note: { optional: true, ...notificationRecieveConfig },
follow: { optional: true, ...notificationRecieveConfig },
mention: { optional: true, ...notificationRecieveConfig },
reply: { optional: true, ...notificationRecieveConfig },
renote: { optional: true, ...notificationRecieveConfig },
quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },
achievementEarned: { optional: true, ...notificationRecieveConfig },
app: { optional: true, ...notificationRecieveConfig },
test: { optional: true, ...notificationRecieveConfig },
},
},
isModerator: {
type: 'boolean',
optional: false, nullable: false,
},
isSilenced: {
type: 'boolean',
optional: false, nullable: false,
},
isSuspended: {
type: 'boolean',
optional: false, nullable: false,
},
isHibernated: {
type: 'boolean',
optional: false, nullable: false,
},
lastActiveDate: {
type: 'string',
optional: false, nullable: true,
},
moderationNote: {
type: 'string',
optional: false, nullable: false,
},
signins: {
type: 'array',
optional: false, nullable: false,
items: {
ref: 'Signin',
},
},
policies: {
type: 'object',
optional: false, nullable: false,
ref: 'RolePolicies',
},
roles: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
ref: 'Role',
},
},
roleAssigns: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
properties: {
createdAt: {
type: 'string',
optional: false, nullable: false,
},
expiresAt: {
type: 'string',
optional: false, nullable: true,
},
roleId: {
type: 'string',
optional: false, nullable: false,
},
},
},
},
},
}, },
} as const; } as const;
@ -89,7 +241,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSilenced: isSilenced, isSilenced: isSilenced,
isSuspended: user.isSuspended, isSuspended: user.isSuspended,
isHibernated: user.isHibernated, isHibernated: user.isHibernated,
lastActiveDate: user.lastActiveDate, lastActiveDate: user.lastActiveDate ? user.lastActiveDate.toISOString() : null,
moderationNote: profile.moderationNote ?? '', moderationNote: profile.moderationNote ?? '',
signins, signins,
policies: await this.roleService.getUserPolicies(user.id), policies: await this.roleService.getUserPolicies(user.id),

View file

@ -24,9 +24,19 @@ export const meta = {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
properties: { properties: {
id: { type: 'string', format: 'misskey:id' }, id: {
score: { type: 'integer' }, type: 'string', format: 'misskey:id',
user: { ref: 'UserLite' }, optional: false, nullable: false,
},
score: {
type: 'integer',
optional: false, nullable: false,
},
user: {
type: 'object',
optional: true, nullable: false,
ref: 'UserLite',
},
}, },
}, },
}, },

View file

@ -29,9 +29,6 @@ export const meta = {
id: 'eb627bc7-574b-4a52-a860-3c3eae772b88', id: 'eb627bc7-574b-4a52-a860-3c3eae772b88',
}, },
}, },
res: {
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -39,7 +36,15 @@ export const paramDef = {
properties: { properties: {
score: { type: 'integer', minimum: 0 }, score: { type: 'integer', minimum: 0 },
seed: { type: 'string', minLength: 1, maxLength: 1024 }, seed: { type: 'string', minLength: 1, maxLength: 1024 },
logs: { type: 'array' }, logs: {
type: 'array',
items: {
type: 'array',
items: {
type: 'number',
},
},
},
gameMode: { type: 'string' }, gameMode: { type: 'string' },
gameVersion: { type: 'integer' }, gameVersion: { type: 'integer' },
}, },

View file

@ -25,8 +25,8 @@ export const meta = {
items: { items: {
type: 'object', type: 'object',
}, },
}, }
}, }
}, },
} as const; } as const;

View file

@ -71,7 +71,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
userId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' },
withReplies: { type: 'boolean' }, withReplies: { type: 'boolean' }
}, },
required: ['userId'], required: ['userId'],
} as const; } as const;

View file

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js'; import type { UsersRepository } from '@/models/_.js';
import { safeForSql } from '@/misc/safe-for-sql.js'; import { safeForSql } from "@/misc/safe-for-sql.js";
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';

View file

@ -14,7 +14,7 @@ export const meta = {
tags: ['account'], tags: ['account'],
requireCredential: true, requireCredential: true,
kind: 'read:account', kind: "read:account",
res: { res: {
type: 'object', type: 'object',

View file

@ -15,6 +15,19 @@ export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
res: {
type: 'object',
properties: {
backupCodes: {
type: 'array',
optional: false,
items: {
type: 'string',
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -47,7 +47,7 @@ export const meta = {
properties: { properties: {
id: { id: {
type: 'string', type: 'string',
nullable: true, optional: true,
}, },
}, },
}, },

View file

@ -21,21 +21,26 @@ export const meta = {
properties: { properties: {
id: { id: {
type: 'string', type: 'string',
optional: false,
format: 'misskey:id', format: 'misskey:id',
}, },
name: { name: {
type: 'string', type: 'string',
optional: true,
}, },
createdAt: { createdAt: {
type: 'string', type: 'string',
optional: false,
format: 'date-time', format: 'date-time',
}, },
lastUsedAt: { lastUsedAt: {
type: 'string', type: 'string',
optional: true,
format: 'date-time', format: 'date-time',
}, },
permission: { permission: {
type: 'array', type: 'array',
optional: false,
uniqueItems: true, uniqueItems: true,
items: { items: {
type: 'string', type: 'string',

View file

@ -23,16 +23,19 @@ export const meta = {
id: { id: {
type: 'string', type: 'string',
format: 'misskey:id', format: 'misskey:id',
optional: false,
}, },
name: { name: {
type: 'string', type: 'string',
optional: false,
}, },
callbackUrl: { callbackUrl: {
type: 'string', type: 'string',
nullable: true, optional: false, nullable: true,
}, },
permission: { permission: {
type: 'array', type: 'array',
optional: false,
uniqueItems: true, uniqueItems: true,
items: { items: {
type: 'string', type: 'string',
@ -40,6 +43,7 @@ export const meta = {
}, },
isAuthorized: { isAuthorized: {
type: 'boolean', type: 'boolean',
optional: true,
}, },
}, },
}, },

View file

@ -22,6 +22,15 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
properties: {
updatedAt: {
type: 'string',
optional: false,
},
value: {
optional: false,
},
},
}, },
} as const; } as const;
@ -50,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
return { return {
updatedAt: item.updatedAt, updatedAt: item.updatedAt.toISOString(),
value: item.value, value: item.value,
}; };
}); });

View file

@ -22,7 +22,7 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
}, }
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -13,6 +13,9 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
additionalProperties: {
type: 'string',
},
}, },
} as const; } as const;

View file

@ -10,6 +10,13 @@ import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
kind: 'read:account', kind: 'read:account',
res: {
type: 'array',
items: {
type: 'string',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -22,8 +22,8 @@ export const meta = {
type: 'array', type: 'array',
items: { items: {
type: 'string', type: 'string',
}, }
}, }
}, },
domain: { domain: {
type: 'string', type: 'string',
@ -31,7 +31,7 @@ export const meta = {
}, },
}, },
}, },
}, }
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -33,6 +33,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { safeForSql } from '@/misc/safe-for-sql.js'; import { safeForSql } from '@/misc/safe-for-sql.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -185,7 +186,26 @@ export const paramDef = {
mutedInstances: { type: 'array', items: { mutedInstances: { type: 'array', items: {
type: 'string', type: 'string',
} }, } },
notificationRecieveConfig: { type: 'object' }, notificationRecieveConfig: {
type: 'object',
nullable: false,
properties: {
note: notificationRecieveConfig,
follow: notificationRecieveConfig,
mention: notificationRecieveConfig,
reply: notificationRecieveConfig,
renote: notificationRecieveConfig,
quote: notificationRecieveConfig,
reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
roleAssigned: notificationRecieveConfig,
achievementEarned: notificationRecieveConfig,
app: notificationRecieveConfig,
test: notificationRecieveConfig,
},
},
emailNotificationTypes: { type: 'array', items: { emailNotificationTypes: { type: 'array', items: {
type: 'string', type: 'string',
} }, } },

View file

@ -108,7 +108,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
url: webhook.url, url: webhook.url,
secret: webhook.secret, secret: webhook.secret,
active: webhook.active, active: webhook.active,
latestSentAt: webhook.latestSentAt?.toISOString(), latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null,
latestStatus: webhook.latestStatus, latestStatus: webhook.latestStatus,
}; };
}); });

View file

@ -73,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
url: webhook.url, url: webhook.url,
secret: webhook.secret, secret: webhook.secret,
active: webhook.active, active: webhook.active,
latestSentAt: webhook.latestSentAt?.toISOString(), latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null,
latestStatus: webhook.latestStatus, latestStatus: webhook.latestStatus,
} }
)); ));

View file

@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
url: webhook.url, url: webhook.url,
secret: webhook.secret, secret: webhook.secret,
active: webhook.active, active: webhook.active,
latestSentAt: webhook.latestSentAt?.toISOString(), latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null,
latestStatus: webhook.latestStatus, latestStatus: webhook.latestStatus,
}; };
}); });

View file

@ -47,7 +47,7 @@ export const meta = {
bothWithRepliesAndWithFiles: { bothWithRepliesAndWithFiles: {
message: 'Specifying both withReplies and withFiles is not supported', message: 'Specifying both withReplies and withFiles is not supported',
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f', id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f'
}, },
}, },
} as const; } as const;

View file

@ -14,9 +14,6 @@ export const meta = {
errors: { errors: {
}, },
res: {
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -30,6 +30,9 @@ export const meta = {
}, },
res: { res: {
type: 'object',
optional: true,
ref: 'ReversiGameDetailed',
}, },
} as const; } as const;

View file

@ -19,20 +19,24 @@ export const meta = {
id: { id: {
type: 'string', type: 'string',
format: 'misskey:id', format: 'misskey:id',
optional: true, nullable: false,
}, },
required: { required: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false,
}, },
string: { string: {
type: 'string', type: 'string',
optional: true, nullable: false,
}, },
default: { default: {
type: 'string', type: 'string',
optional: true, nullable: false,
}, },
nullableDefault: { nullableDefault: {
type: 'string', type: 'string',
default: 'hello', default: 'hello',
nullable: true, optional: true, nullable: true,
}, },
}, },
}, },

View file

@ -43,6 +43,7 @@ import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { deepClone } from '@/misc/clone.js'; import { deepClone } from '@/misc/clone.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
@ -264,11 +265,16 @@ export class ClientServerService {
//#region vite assets //#region vite assets
if (this.config.clientManifestExists) { if (this.config.clientManifestExists) {
fastify.register(fastifyStatic, { fastify.register((fastify, options, done) => {
root: viteOut, fastify.register(fastifyStatic, {
prefix: '/vite/', root: viteOut,
maxAge: ms('30 days'), prefix: '/vite/',
decorateReply: false, maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
done();
}); });
} else { } else {
const port = (process.env.VITE_PORT ?? '5173'); const port = (process.env.VITE_PORT ?? '5173');
@ -303,11 +309,16 @@ export class ClientServerService {
decorateReply: false, decorateReply: false,
}); });
fastify.register(fastifyStatic, { fastify.register((fastify, options, done) => {
root: tarball, fastify.register(fastifyStatic, {
prefix: '/tarball/', root: tarball,
immutable: true, prefix: '/tarball/',
decorateReply: false, maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
done();
}); });
fastify.get('/favicon.ico', async (request, reply) => { fastify.get('/favicon.ico', async (request, reply) => {

View file

@ -7,10 +7,11 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { MiNote } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js';
import type { Packed } from '@/misc/json-schema.js';
import { api, initTestDb, makeStreamCatcher, post, signup, uploadFile } from '../utils.js'; import { api, initTestDb, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
import type{ Repository } from 'typeorm'; import type{ Repository } from 'typeorm'
import type { Packed } from '@/misc/json-schema.js';
describe('Drive', () => { describe('Drive', () => {
let Notes: Repository<MiNote>; let Notes: Repository<MiNote>;
@ -30,7 +31,7 @@ describe('Drive', () => {
const marker = Math.random().toString(); const marker = Math.random().toString();
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'; const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'
const catcher = makeStreamCatcher( const catcher = makeStreamCatcher(
alice, alice,
@ -50,7 +51,7 @@ describe('Drive', () => {
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
assert.strictEqual(file.name, 'Lenna.jpg'); assert.strictEqual(file.name, 'Lenna.jpg');
assert.strictEqual(file.type, 'image/jpeg'); assert.strictEqual(file.type, 'image/jpeg');
}); })
test('ローカルからアップロードできる', async () => { test('ローカルからアップロードできる', async () => {
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする // APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
@ -58,27 +59,27 @@ describe('Drive', () => {
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' }); const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' });
assert.strictEqual(res.body?.name, 'テスト画像.jpg'); assert.strictEqual(res.body?.name, 'テスト画像.jpg');
assert.strictEqual(res.body.type, 'image/jpeg'); assert.strictEqual(res.body?.type, 'image/jpeg');
}); })
test('添付ノート一覧を取得できる', async () => { test('添付ノート一覧を取得できる', async () => {
const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id); const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id)
const note0 = await post(alice, { fileIds: [ids[0]] }); const note0 = await post(alice, { fileIds: [ids[0]] });
const note1 = await post(alice, { fileIds: [ids[0], ids[1]] }); const note1 = await post(alice, { fileIds: [ids[0], ids[1]] });
const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice); const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice);
assert.strictEqual(attached0.body.length, 2); assert.strictEqual(attached0.body.length, 2);
assert.strictEqual(attached0.body[0].id, note1.id); assert.strictEqual(attached0.body[0].id, note1.id)
assert.strictEqual(attached0.body[1].id, note0.id); assert.strictEqual(attached0.body[1].id, note0.id)
const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice); const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice);
assert.strictEqual(attached1.body.length, 1); assert.strictEqual(attached1.body.length, 1);
assert.strictEqual(attached1.body[0].id, note1.id); assert.strictEqual(attached1.body[0].id, note1.id)
const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice); const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice);
assert.strictEqual(attached2.body.length, 0); assert.strictEqual(attached2.body.length, 0)
}); })
test('添付ノート一覧は他の人から見えない', async () => { test('添付ノート一覧は他の人から見えない', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
@ -88,6 +89,7 @@ describe('Drive', () => {
const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob); const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual('error' in res.body, true); assert.strictEqual('error' in res.body, true);
});
})
}); });

View file

@ -13,10 +13,10 @@ import fetch, { File, RequestInit } from 'node-fetch';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { Packed } from '@/misc/json-schema.js';
import { entities } from '../src/postgres.js'; import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js'; import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
import { Packed } from '@/misc/json-schema.js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
@ -123,9 +123,9 @@ export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', len
function timeoutPromise<T>(p: Promise<T>, timeout: number): Promise<T> { function timeoutPromise<T>(p: Promise<T>, timeout: number): Promise<T> {
return Promise.race([ return Promise.race([
p, p,
new Promise((reject) => { new Promise((reject) =>{
setTimeout(() => { reject(new Error('timed out')); }, timeout); setTimeout(() => { reject(new Error('timed out')); }, timeout)
}) as never, }) as never
]); ]);
} }
@ -343,7 +343,7 @@ export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'D
'main', 'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>, (msg) => msg.body.file as Packed<'DriveFile'>,
60 * 1000, 60 * 1000
); );
await api('drive/files/upload-from-url', { await api('drive/files/upload-from-url', {
@ -434,20 +434,20 @@ export const waitFire = async (user: UserToken, channel: string, trgr: () => any
* @returns extractorを通した値を得る * @returns extractorを通した値を得る
*/ */
export function makeStreamCatcher<T>( export function makeStreamCatcher<T>(
user: UserToken, user: UserToken,
channel: string, channel: string,
cond: (message: Record<string, any>) => boolean, cond: (message: Record<string, any>) => boolean,
extractor: (message: Record<string, any>) => T, extractor: (message: Record<string, any>) => T,
timeout = 60 * 1000): Promise<T> { timeout = 60 * 1000): Promise<T> {
let ws: WebSocket; let ws: WebSocket
const p = new Promise<T>(async (resolve) => { const p = new Promise<T>(async (resolve) => {
ws = await connectStream(user, channel, (msg) => { ws = await connectStream(user, channel, (msg) => {
if (cond(msg)) { if (cond(msg)) {
resolve(extractor(msg)); resolve(extractor(msg))
} }
}); });
}).finally(() => { }).finally(() => {
ws.close(); ws?.close();
}); });
return timeoutPromise(p, timeout); return timeoutPromise(p, timeout);

View file

@ -3,25 +3,28 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { resolve } from 'node:path'; import { createRequire } from 'node:module';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import type { StorybookConfig } from '@storybook/vue3-vite'; import type { StorybookConfig } from '@storybook/vue3-vite';
import { type Plugin, mergeConfig } from 'vite'; import { type Plugin, mergeConfig } from 'vite';
import turbosnap from 'vite-plugin-turbosnap'; import turbosnap from 'vite-plugin-turbosnap';
const dirname = fileURLToPath(new URL('.', import.meta.url)); const require = createRequire(import.meta.url);
const _dirname = fileURLToPath(new URL('.', import.meta.url));
const config = { const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [ addons: [
'@storybook/addon-essentials', getAbsolutePath('@storybook/addon-essentials'),
'@storybook/addon-interactions', getAbsolutePath('@storybook/addon-interactions'),
'@storybook/addon-links', getAbsolutePath('@storybook/addon-links'),
'@storybook/addon-storysource', getAbsolutePath('@storybook/addon-storysource'),
resolve(dirname, '../node_modules/storybook-addon-misskey-theme'), getAbsolutePath('@storybook/addon-mdx-gfm'),
resolve(_dirname, '../node_modules/storybook-addon-misskey-theme'),
], ],
framework: { framework: {
name: '@storybook/vue3-vite', name: getAbsolutePath('@storybook/vue3-vite') as '@storybook/vue3-vite',
options: {}, options: {},
}, },
docs: { docs: {
@ -37,10 +40,13 @@ const config = {
} }
return mergeConfig(config, { return mergeConfig(config, {
plugins: [ plugins: [
// XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8 {
(turbosnap as any as typeof turbosnap['default'])({ // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8
rootDir: config.root ?? process.cwd(), ...(turbosnap as any as typeof turbosnap['default'])({
}), rootDir: config.root ?? process.cwd(),
}),
name: 'fake-turbosnap',
},
], ],
build: { build: {
target: [ target: [
@ -53,3 +59,7 @@ const config = {
}, },
} satisfies StorybookConfig; } satisfies StorybookConfig;
export default config; export default config;
function getAbsolutePath(value: string): string {
return dirname(require.resolve(join(value, 'package.json')));
}

View file

@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { addons } from '@storybook/addons';
import { FORCE_REMOUNT } from '@storybook/core-events'; import { FORCE_REMOUNT } from '@storybook/core-events';
import { addons } from '@storybook/preview-api';
import { type Preview, setup } from '@storybook/vue3'; import { type Preview, setup } from '@storybook/vue3';
import isChromatic from 'chromatic/isChromatic'; import isChromatic from 'chromatic/isChromatic';
import { initialize, mswDecorator } from 'msw-storybook-addon'; import { initialize, mswDecorator } from 'msw-storybook-addon';

View file

@ -8,7 +8,7 @@
"build": "vite build", "build": "vite build",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
"build-storybook": "pnpm build-storybook-pre && storybook build", "build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
"chromatic": "chromatic", "chromatic": "chromatic",
"test": "vitest --run --globals", "test": "vitest --run --globals",
"test-and-coverage": "vitest --run --coverage --globals", "test-and-coverage": "vitest --run --coverage --globals",
@ -28,7 +28,7 @@
"@tabler/icons-webfont": "2.44.0", "@tabler/icons-webfont": "2.44.0",
"@twemoji/parser": "15.0.0", "@twemoji/parser": "15.0.0",
"@vitejs/plugin-vue": "5.0.3", "@vitejs/plugin-vue": "5.0.3",
"@vue/compiler-sfc": "3.4.15", "@vue/compiler-sfc": "3.4.18",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.2", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.2",
"astring": "1.8.6", "astring": "1.8.6",
"broadcast-channel": "7.0.0", "broadcast-channel": "7.0.0",
@ -72,30 +72,30 @@
"uuid": "9.0.1", "uuid": "9.0.1",
"v-code-diff": "1.7.2", "v-code-diff": "1.7.2",
"vite": "5.1.0", "vite": "5.1.0",
"vue": "3.4.15", "vue": "3.4.18",
"vuedraggable": "next" "vuedraggable": "next"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@misskey-dev/summaly": "5.0.3", "@misskey-dev/summaly": "5.0.3",
"@storybook/addon-actions": "7.6.10", "@storybook/addon-actions": "8.0.0-beta.2",
"@storybook/addon-essentials": "7.6.10", "@storybook/addon-essentials": "8.0.0-beta.2",
"@storybook/addon-interactions": "7.6.10", "@storybook/addon-interactions": "8.0.0-beta.2",
"@storybook/addon-links": "7.6.10", "@storybook/addon-links": "8.0.0-beta.2",
"@storybook/addon-storysource": "7.6.10", "@storybook/addon-mdx-gfm": "8.0.0-beta.2",
"@storybook/addons": "7.6.10", "@storybook/addon-storysource": "8.0.0-beta.2",
"@storybook/blocks": "7.6.10", "@storybook/blocks": "8.0.0-beta.2",
"@storybook/core-events": "7.6.10", "@storybook/components": "8.0.0-beta.2",
"@storybook/jest": "0.2.3", "@storybook/core-events": "8.0.0-beta.2",
"@storybook/manager-api": "7.6.10", "@storybook/manager-api": "8.0.0-beta.2",
"@storybook/preview-api": "7.6.10", "@storybook/preview-api": "8.0.0-beta.2",
"@storybook/react": "7.6.10", "@storybook/react": "8.0.0-beta.2",
"@storybook/react-vite": "7.6.10", "@storybook/react-vite": "8.0.0-beta.2",
"@storybook/testing-library": "0.2.2", "@storybook/test": "8.0.0-beta.2",
"@storybook/theming": "7.6.10", "@storybook/theming": "8.0.0-beta.2",
"@storybook/types": "7.6.10", "@storybook/types": "8.0.0-beta.2",
"@storybook/vue3": "7.6.10", "@storybook/vue3": "8.0.0-beta.2",
"@storybook/vue3-vite": "7.6.10", "@storybook/vue3-vite": "8.0.0-beta.2",
"@testing-library/vue": "8.0.2", "@testing-library/vue": "8.0.2",
"@types/escape-regexp": "0.0.3", "@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
@ -129,12 +129,12 @@
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"start-server-and-test": "2.0.3", "start-server-and-test": "2.0.3",
"storybook": "7.6.10", "storybook": "8.0.0-beta.2",
"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": "0.34.6", "vitest": "0.34.6",
"vitest-fetch-mock": "0.2.2", "vitest-fetch-mock": "0.2.2",
"vue-component-type-helpers": "^1.8.27", "vue-component-type-helpers": "1.8.27",
"vue-eslint-parser": "9.4.2", "vue-eslint-parser": "9.4.2",
"vue-tsc": "1.8.27" "vue-tsc": "1.8.27"
} }

View file

@ -5,8 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { expect } from '@storybook/jest'; import { expect, userEvent, waitFor, within } from '@storybook/test';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js'; import { userDetailed } from '../../.storybook/fakes.js';

View file

@ -123,7 +123,7 @@ function callback(response?: string) {
function onReceivedMessage(message: MessageEvent) { function onReceivedMessage(message: MessageEvent) {
if (message.data.token) { if (message.data.token) {
if (props.instanceUrl && new URL(message.origin).host === new URL(props.instanceUrl).host) { if (props.instanceUrl && new URL(message.origin).host === new URL(props.instanceUrl).host) {
callback(<string>message.data.token); callback(message.data.token);
} }
} }
} }

View file

@ -4,8 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest'; import { expect, userEvent, waitFor, within } from '@storybook/test';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { galleryPost } from '../../.storybook/fakes.js'; import { galleryPost } from '../../.storybook/fakes.js';
import MkGalleryPostPreview from './MkGalleryPostPreview.vue'; import MkGalleryPostPreview from './MkGalleryPostPreview.vue';

View file

@ -16,9 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<template #header> <template #header>
<template v-if="pageMetadata?.value"> <template v-if="pageMetadata">
<i v-if="pageMetadata.value.icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i> <i v-if="pageMetadata.icon" :class="pageMetadata.icon" style="margin-right: 0.5em;"></i>
<span>{{ pageMetadata.value.title }}</span> <span>{{ pageMetadata.title }}</span>
</template> </template>
</template> </template>
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ComputedRef, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'; import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
import RouterView from '@/components/global/RouterView.vue'; import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js'; import { popout as _popout } from '@/scripts/popout.js';
@ -37,7 +37,7 @@ import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { useScrollPositionManager } from '@/nirax.js'; import { useScrollPositionManager } from '@/nirax.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { openingWindowsCount } from '@/os.js'; import { openingWindowsCount } from '@/os.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { getScrollContainer } from '@/scripts/scroll.js'; import { getScrollContainer } from '@/scripts/scroll.js';
@ -56,7 +56,7 @@ const routerFactory = useRouterFactory();
const windowRouter = routerFactory(props.initialPath); const windowRouter = routerFactory(props.initialPath);
const contents = shallowRef<HTMLElement | null>(null); const contents = shallowRef<HTMLElement | null>(null);
const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const pageMetadata = ref<null | PageMetadata>(null);
const windowEl = shallowRef<InstanceType<typeof MkWindow>>(); const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
const history = ref<{ path: string; key: any; }[]>([{ const history = ref<{ path: string; key: any; }[]>([{
path: windowRouter.getCurrentPath(), path: windowRouter.getCurrentPath(),
@ -101,9 +101,11 @@ windowRouter.addListener('replace', ctx => {
windowRouter.init(); windowRouter.init();
provide('router', windowRouter); provide('router', windowRouter);
provideMetadataReceiver((info) => { provideMetadataReceiver((metadataGetter) => {
const info = metadataGetter();
pageMetadata.value = info; pageMetadata.value = info;
}); });
provideReactiveMetadata(pageMetadata);
provide('shouldOmitHeaderTitle', true); provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true); provide('shouldHeaderThin', true);
provide('forceSpacerMin', true); provide('forceSpacerMin', true);

View file

@ -4,8 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest'; import { expect, userEvent, waitFor, within } from '@storybook/test';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { onBeforeUnmount } from 'vue'; import { onBeforeUnmount } from 'vue';
import MkSignupServerRules from './MkSignupDialog.rules.vue'; import MkSignupServerRules from './MkSignupDialog.rules.vue';

View file

@ -4,8 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest'; import { expect, userEvent, within } from '@storybook/test';
import { userEvent, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import MkA from './MkA.vue'; import MkA from './MkA.vue';
import { tick } from '@/scripts/test-utils.js'; import { tick } from '@/scripts/test-utils.js';

View file

@ -5,8 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { expect } from '@storybook/jest'; import { expect, waitFor } from '@storybook/test';
import { waitFor } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import MkError from './MkError.vue'; import MkError from './MkError.vue';
export const Default = { export const Default = {

View file

@ -5,8 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { within } from '@storybook/testing-library'; import { expect, within } from '@storybook/test';
import { expect } from '@storybook/jest';
import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.js'; import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.js';
export const Default = { export const Default = {
render(args) { render(args) {

View file

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { waitFor } from '@storybook/testing-library'; import { waitFor } from '@storybook/test';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import MkPageHeader from './MkPageHeader.vue'; import MkPageHeader from './MkPageHeader.vue';
export const Empty = { export const Empty = {

View file

@ -11,18 +11,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div v-else-if="!thin_ && narrow && !hideTitle"/> <div v-else-if="!thin_ && narrow && !hideTitle"/>
<template v-if="metadata"> <template v-if="pageMetadata">
<div v-if="!hideTitle && !hide" :class="$style.titleContainer" @click="top"> <div v-if="!hideTitle && !hide" :class="$style.titleContainer" @click="top">
<div v-if="metadata.avatar" :class="$style.titleAvatarContainer"> <div v-if="pageMetadata.avatar" :class="$style.titleAvatarContainer">
<MkAvatar :class="$style.titleAvatar" :user="metadata.avatar" indicator/> <MkAvatar :class="$style.titleAvatar" :user="pageMetadata.avatar" indicator/>
</div> </div>
<i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i> <i v-else-if="pageMetadata.icon" :class="[$style.titleIcon, pageMetadata.icon]"></i>
<div :class="$style.title"> <div :class="$style.title">
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true"/> <MkUserName v-if="pageMetadata.userName" :user="pageMetadata.userName" :nowrap="true"/>
<div v-else-if="metadata.title">{{ metadata.title }}</div> <div v-else-if="pageMetadata.title">{{ pageMetadata.title }}</div>
<div v-if="metadata.subtitle" :class="$style.subtitle"> <div v-if="pageMetadata.subtitle" :class="$style.subtitle">
{{ metadata.subtitle }} {{ pageMetadata.subtitle }}
</div> </div>
</div> </div>
</div> </div>
@ -46,7 +46,7 @@ import tinycolor from 'tinycolor2';
import XTabs, { Tab } from './MkPageHeader.tabs.vue'; import XTabs, { Tab } from './MkPageHeader.tabs.vue';
import { scrollToTop } from '@/scripts/scroll.js'; import { scrollToTop } from '@/scripts/scroll.js';
import { globalEvents } from '@/events.js'; import { globalEvents } from '@/events.js';
import { injectPageMetadata } from '@/scripts/page-metadata.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
import { PageHeaderItem } from '@/types/page-header.js'; import { PageHeaderItem } from '@/types/page-header.js';
@ -65,7 +65,7 @@ const emit = defineEmits<{
(ev: 'update:tab', key: string); (ev: 'update:tab', key: string);
}>(); }>();
const metadata = injectPageMetadata(); const pageMetadata = injectReactiveMetadata();
const hideTitle = inject('shouldOmitHeaderTitle', false); const hideTitle = inject('shouldOmitHeaderTitle', false);
const thin_ = props.thin || inject('shouldHeaderThin', false); const thin_ = props.thin || inject('shouldHeaderThin', false);

View file

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest'; import { expect } from '@storybook/test';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import MkTime from './MkTime.vue'; import MkTime from './MkTime.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View file

@ -4,8 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest'; import { expect, userEvent, waitFor, within } from '@storybook/test';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../../.storybook/mocks.js'; import { commonHandlers } from '../../../.storybook/mocks.js';

View file

@ -4,7 +4,7 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest'; import { expect } from '@storybook/test';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { userDetailed } from '../../../.storybook/fakes.js'; import { userDetailed } from '../../../.storybook/fakes.js';
import MkUserName from './MkUserName.vue'; import MkUserName from './MkUserName.vue';

View file

@ -18,7 +18,7 @@ export const langs = _LANGS_;
const preParseLocale = miLocalStorage.getItem('locale'); const preParseLocale = miLocalStorage.getItem('locale');
export let locale = preParseLocale ? JSON.parse(preParseLocale) : null; export let locale = preParseLocale ? JSON.parse(preParseLocale) : null;
export const version = _VERSION_; export const version = _VERSION_;
export const instanceName = siteName === 'Misskey' ? host : siteName; export const instanceName = siteName === 'Misskey' || siteName == null ? host : siteName;
export const ui = miLocalStorage.getItem('ui'); export const ui = miLocalStorage.getItem('ui');
export const debug = miLocalStorage.getItem('debug') === 'true'; export const debug = miLocalStorage.getItem('debug') === 'true';

View file

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>{{ i18n.ts.youShouldUpgradeClient }}</div> <div>{{ i18n.ts.youShouldUpgradeClient }}</div>
<MkButton style="margin: 8px auto;" @click="reload">{{ i18n.ts.reload }}</MkButton> <MkButton style="margin: 8px auto;" @click="reload">{{ i18n.ts.reload }}</MkButton>
</template> </template>
<div><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></div> <div><MkLink url="https://misskey-hub.net/docs/for-users/resources/troubleshooting/" target="_blank">{{ i18n.ts.troubleshooting }}</MkLink></div>
<div v-if="error" style="opacity: 0.7;">ERROR: {{ error }}</div> <div v-if="error" style="opacity: 0.7;">ERROR: {{ error }}</div>
</div> </div>
</div> </div>
@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkLink from '@/components/MkLink.vue';
import { version } from '@/config.js'; import { version } from '@/config.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload } from '@/scripts/unison-reload.js'; import { unisonReload } from '@/scripts/unison-reload.js';
@ -66,10 +67,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);
definePageMetadata({ definePageMetadata(() => ({
title: i18n.ts.error, title: i18n.ts.error,
icon: 'ti ti-alert-triangle', icon: 'ti ti-alert-triangle',
}); }));
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -367,10 +367,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);
definePageMetadata({ definePageMetadata(() => ({
title: i18n.ts.aboutMisskey, title: i18n.ts.aboutMisskey,
icon: null, icon: null,
}); }));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -182,10 +182,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-chart-line', icon: 'ti ti-chart-line',
}]); }]);
definePageMetadata(computed(() => ({ definePageMetadata(() => ({
title: i18n.ts.instanceInfo, title: i18n.ts.instanceInfo,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',
}))); }));
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -48,10 +48,10 @@ onDeactivated(() => {
} }
}); });
definePageMetadata({ definePageMetadata(() => ({
title: i18n.ts.achievements, title: i18n.ts.achievements,
icon: 'ti ti-medal', icon: 'ti ti-medal',
}); }));
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -140,10 +140,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-code', icon: 'ti ti-code',
}]); }]);
definePageMetadata(computed(() => ({ definePageMetadata(() => ({
title: file.value ? i18n.ts.file + ': ' + file.value.name : i18n.ts.file, title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
icon: 'ti ti-file', icon: 'ti ti-file',
}))); }));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -518,10 +518,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-code', icon: 'ti ti-code',
}]); }]);
definePageMetadata(computed(() => ({ definePageMetadata(() => ({
title: user.value ? acct(user.value) : i18n.ts.userInfo, title: user.value ? acct(user.value) : i18n.ts.userInfo,
icon: 'ti ti-user-exclamation', icon: 'ti ti-user-exclamation',
}))); }));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick"> <div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick">
<template v-if="metadata"> <template v-if="pageMetadata">
<div class="titleContainer" @click="showTabsPopup"> <div class="titleContainer" @click="showTabsPopup">
<i v-if="metadata.icon" class="icon" :class="metadata.icon"></i> <i v-if="pageMetadata.icon" class="icon" :class="pageMetadata.icon"></i>
<div class="title"> <div class="title">
<div class="title">{{ metadata.title }}</div> <div class="title">{{ pageMetadata.title }}</div>
</div> </div>
</div> </div>
<div class="tabs"> <div class="tabs">
@ -39,7 +39,7 @@ import { popupMenu } from '@/os.js';
import { scrollToTop } from '@/scripts/scroll.js'; import { scrollToTop } from '@/scripts/scroll.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { globalEvents } from '@/events.js'; import { globalEvents } from '@/events.js';
import { injectPageMetadata } from '@/scripts/page-metadata.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
type Tab = { type Tab = {
key?: string | null; key?: string | null;
@ -65,7 +65,7 @@ const emit = defineEmits<{
(ev: 'update:tab', key: string); (ev: 'update:tab', key: string);
}>(); }>();
const metadata = injectPageMetadata(); const pageMetadata = injectReactiveMetadata();
const el = shallowRef<HTMLElement>(null); const el = shallowRef<HTMLElement>(null);
const tabRefs = {}; const tabRefs = {};
@ -118,7 +118,7 @@ function onTabClick(tab: Tab, ev: MouseEvent): void {
} }
const calcBg = () => { const calcBg = () => {
const rawBg = metadata?.bg ?? 'var(--bg)'; const rawBg = pageMetadata.value?.bg ?? 'var(--bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85); tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString(); bg.value = tinyBg.toRgbString();

View file

@ -87,8 +87,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);
definePageMetadata({ definePageMetadata(() => ({
title: i18n.ts.abuseReports, title: i18n.ts.abuseReports,
icon: 'ti ti-exclamation-circle', icon: 'ti ti-exclamation-circle',
}); }));
</script> </script>

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