Merge branch 'develop' into detect-size

This commit is contained in:
FineArchs 2024-03-03 17:51:05 +09:00 committed by GitHub
commit 51a10e55d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
390 changed files with 8746 additions and 5778 deletions

View File

@ -2,6 +2,63 @@
# Misskey configuration # Misskey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌──────────────────────────────┐
#───┘ a boring but important thing └────────────────────────────
#
# First of all, let me tell you a story that may possibly be
# boring to you and possibly important to you.
#
# Misskey is licensed under the AGPLv3 license. This license is
# known to be often misunderstood. Please read the following
# instructions carefully and select the appropriate option so
# that you do not negligently cause a license violation.
#
# --------
# Option 1: If you host Misskey AS-IS (without any changes to
# the source code. forks are not included).
#
# Step 1: Congratulations! You don't need to do anything.
# --------
# Option 2: If you have made changes to the source code (forks
# are included) and publish a Git repository of source
# code. There should be no access restrictions on
# this repository. Strictly speaking, it doesn't have
# to be a Git repository, but you'll probably use Git!
#
# Step 1: Build and run the Misskey server first.
# Step 2: Open <https://your.misskey.example/admin/settings> in
# your browser with the administrator account.
# Step 3: Enter the URL of your Git repository in the
# "Repository URL" field.
# --------
# Option 3: If neither of the above applies to you.
# (In this case, the source code should be published
# on the Misskey interface. IT IS NOT ENOUGH TO
# DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY
# E-MAIL OR OTHER MEANS. If you are not satisfied
# with this, it is recommended that you read the
# license again carefully. Anyway, enabling this
# option will automatically generate and publish a
# tarball at build time, protecting you from
# inadvertent license violations. (There is no legal
# guarantee, of course.) The tarball will generated
# from the root directory of your codebase. So it is
# also recommended to check <built/tarball> directory
# once after building and before activating the server
# to avoid ACCIDENTAL LEAKING OF SENSITIVE INFORMATION.
# To prevent certain files from being included in the
# tarball, add a glob pattern after line 15 in
# <scripts/tarball.mjs>. DO NOT FORGET TO BUILD AFTER
# ENABLING THIS OPTION!)
#
# Step 1: Uncomment the following line.
#
# publishTarballInsteadOfProvideRepositoryUrl: true
# ┌─────┐ # ┌─────┐
#───┘ URL └───────────────────────────────────────────────────── #───┘ URL └─────────────────────────────────────────────────────

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
steps:
- name: Check out the repo
uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx
id: buildx
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
tags: |
type=edge type=edge
type=ref,event=pr type=ref,event=pr
type=ref,event=branch type=ref,event=branch
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} 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:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v4.1.1
- 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: 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

@ -12,18 +12,73 @@
--> -->
## 202x.x.x (Unreleased) ## 2024.3.1
### General
-
### Client
- Fix: 絵文字関係の不具合を修正 (#13485)
- 履歴に残っている or ピン留めされた絵文字がコントロールパネルより削除されていた際にリアクションデッキが表示できなくなる
- Unicode絵文字が履歴に残っている or ピン留めされているとリアクションデッキが表示できなくなる
- Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487
### Server
-
## 2024.3.0
### General
- Enhance: 投稿者のロールに応じて、一つのノートに含むことのできるメンションとダイレクト投稿の宛先の人数に上限を設定できるように
* デフォルトのメンション上限は20アカウントに設定されます。管理者はベースロールの設定で変更可能です。
* 連合の問い合わせに応答しないサーバーのリモートユーザーへのメンションは、上限の人数に含めない実装になっています。
- Enhance: 通知がミュート、凍結を考慮するようになりました
- Enhance: サーバーごとにモデレーションノートを残せるように
- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加
- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加
- Enhance: 通知の履歴をリセットできるように
- Fix: ダイレクトなノートに対してはダイレクトでしか返信できないように
### Client
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
- Fix: syuilo/misskeyの時代からあるインスタンスが改変されたバージョンであると誤認識される問題
- Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正
- Fix: チャートのラベルが消えている問題を修正
- Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正
- Fix: 設定のバックアップ作成時に名前を入力しなかった場合、ローカライゼーションがおかしくなる問題を修正
- Fix: ページ`/admin/emojis`の絵文字編集ダイアログで「リアクションとして使えるロール」を追加する際に何も選択せずOKを押下すると画面が固まる問題を修正
- Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正
- Fix: ユーザの情報のポップアップが消えなくなることがある問題を修正
### Server
- Enhance: エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました
- Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正
- Fix: 破損した通知をクライアントに送信しないように
* 通知欄が無限にリロードされる問題が改善する可能性があります
- Fix: 禁止キーワードを含むートがDelayed Queueに追加されて再処理される問題を修正
- Fix: 自分がフォローしていないアカウントのフォロワー限定ノートが閲覧できることがある問題を修正
- Fix: タイムラインのオプションで「リノートを表示」を無効にしている際、投票のみの引用リノートが流れてこない問題を修正
- Fix: エンドポイント`admin/emoji/update`の各種修正
- 必須パラメータを`id`または`name`のいずれかのみに
- `id`の代わりに`name`で絵文字を指定可能に(`id`・`name`両指定時は従来通り`name`を変更する挙動)
- `category`および`licence`が指定なしの時勝手にnullに上書きされる挙動を修正
- Fix: 通知の受信設定で「相互フォロー」が正しく動作しない問題を修正
## 2024.2.0
### Note ### Note
- 外部サイトからプラグインをインストールする場合のパスが`/install-extentions`から`/install-extensions`に変わります。現時点では以前のパスも利用できますが、非推奨です。 - 外部サイトからプラグインをインストールする場合のパスが`/install-extentions`から`/install-extensions`に変わります。以前のパスからは自動でリダイレクトされるようになっていますが、新しいパスに変更することをお勧めします。
### 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
- Feat: AGPLv3ライセンスに誤って違反するのを防止する機能を追加
- 管理者がrepositoryUrlを変更したり、またはソースコードを直接頒布することを選択できるようになります
- 本体のソースコードに改変を加えた際に、ライセンスに基づく適切な案内を表示します
- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
- Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正 - Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正
* すべてのリモートユーザーのリアクション一覧を見えないようにします * すべてのリモートユーザーのリアクション一覧を見えないようにします
- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように
- Fix: 特定のキーワード及び正規表現にマッチする文字列を含むノートが投稿された際、エラーに出来るような設定項目を追加 #13207 - Fix: 特定のキーワード及び正規表現にマッチする文字列を含むノートが投稿された際、エラーに出来るような設定項目を追加 #13207
* デフォルトは空欄なので適用前と同等の動作になります * デフォルトは空欄なので適用前と同等の動作になります
@ -57,6 +112,10 @@
- リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合 - リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合
- センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合 - センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合
- ロールが必要な絵文字をリアクションしようとした場合 - ロールが必要な絵文字をリアクションしようとした場合
- Enhance: ページ遷移時にPlayerを閉じるように
- Enhance: 通報ページのユーザをクリックした際にユーザをウィンドウで開くように
- Enhance: ノートの通報時にリモートのノートであっても自インスタンスにおけるノートのリンクを含むように
- Enhance: オフライン表示のデザインを改善・多言語対応
- 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 +126,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 +137,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,10 +150,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
- Enhance: オフライン表示のデザインを改善・多言語対応
## 2023.12.2 ## 2023.12.2

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

@ -1,9 +1,11 @@
<div align="center"> <div align="center">
<a href="https://misskey-hub.net"> <a href="https://misskey-hub.net">
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/> <img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="300"/>
</a> </a>
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀** **🌎 **Misskey** is an open source, federated social media platform that's free forever! 🚀**
[Learn more](https://misskey-hub.net/)
--- ---
@ -22,41 +24,6 @@
<a href="https://www.patreon.com/syuilo"> <a href="https://www.patreon.com/syuilo">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a> <img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
---
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)
</div>
<div>
<a href="https://xn--931a.moe/"><img src="https://github.com/misskey-dev/misskey/blob/develop/assets/ai.png?raw=true" align="right" height="320px"/></a>
## ✨ Features
- **ActivityPub support**\
Not on Misskey? No problem! Not only can Misskey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed!
- **Reactions**\
You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button.
- **Drive**\
With Misskey's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made!
- **Rich Web UI**\
Misskey has a rich and easy to use Web UI!
It is highly customizable, from changing the layout and adding widgets to making custom themes.
Furthermore, plugins can be created using AiScript, an original programming language.
- And much more...
</div>
<div style="clear: both;"></div>
## Documentation
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/docs/), some of the links and graphics above also lead to specific portions of it.
## Sponsors
<div align="center">
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
</div> </div>
## Thanks ## Thanks

View File

@ -1011,8 +1011,10 @@ expired: "منتهية صلاحيته"
icon: "الصورة الرمزية" icon: "الصورة الرمزية"
replies: "رد" replies: "رد"
renotes: "أعد النشر" renotes: "أعد النشر"
sourceCode: "الشفرة المصدرية"
flip: "اقلب" flip: "اقلب"
lastNDays: "آخر {n} أيام" lastNDays: "آخر {n} أيام"
surrender: "ألغِ"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "نجح إنشاء حسابك!" accountCreated: "نجح إنشاء حسابك!"
letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي." letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي."

View File

@ -855,6 +855,7 @@ youFollowing: "অনুসরণ করা হচ্ছে"
icon: "প্রোফাইল ছবি" icon: "প্রোফাইল ছবি"
replies: "জবাব" replies: "জবাব"
renotes: "রিনোট" renotes: "রিনোট"
sourceCode: "সোর্স কোড"
flip: "উল্টান" flip: "উল্টান"
_role: _role:
priority: "অগ্রাধিকার" priority: "অগ্রাধিকার"

View File

@ -1167,6 +1167,7 @@ hideRepliesToOthersInTimelineAll: "Ocultar les teves respostes a tots els usuari
confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les teves respostes a tots els que segueixes a la teva línia de temps?" confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les teves respostes a tots els que segueixes a la teva línia de temps?"
confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps?" confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps?"
externalServices: "Serveis externs" externalServices: "Serveis externs"
sourceCode: "Codi font"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "Adreça URL impressum" impressumUrl: "Adreça URL impressum"
impressumDescription: "A països, com Alemanya, la inclusió de la informació de contacte de l'operador (un Impressum) és requereix de manera legal per llocs comercials." impressumDescription: "A països, com Alemanya, la inclusió de la informació de contacte de l'operador (un Impressum) és requereix de manera legal per llocs comercials."
@ -1209,6 +1210,7 @@ hemisphere: "Geolocalització"
withSensitive: "Incloure notes amb fitxers sensibles" withSensitive: "Incloure notes amb fitxers sensibles"
userSaysSomethingSensitive: "La publicació de {name} conte material sensible" userSaysSomethingSensitive: "La publicació de {name} conte material sensible"
enableHorizontalSwipe: "Lliscar per canviar de pestanya" enableHorizontalSwipe: "Lliscar per canviar de pestanya"
surrender: "Cancel·lar "
_bubbleGame: _bubbleGame:
howToPlay: "Com es juga" howToPlay: "Com es juga"
_howToPlay: _howToPlay:

View File

@ -1095,8 +1095,10 @@ iHaveReadXCarefullyAndAgree: "Přečetl jsem si text \"{x}\" a souhlasím s ním
icon: "Avatar" icon: "Avatar"
replies: "Odpovědět" replies: "Odpovědět"
renotes: "Přeposlat" renotes: "Přeposlat"
sourceCode: "Zdrojový kód"
flip: "Otočit" flip: "Otočit"
lastNDays: "Posledních {n} dnů" lastNDays: "Posledních {n} dnů"
surrender: "Zrušit"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Váš účet byl úspěšně vytvořen!" accountCreated: "Váš účet byl úspěšně vytvořen!"
letsStartAccountSetup: "Pro začátek si nastavte svůj profil." letsStartAccountSetup: "Pro začátek si nastavte svůj profil."

View File

@ -1158,6 +1158,7 @@ hideRepliesToOthersInTimelineAll: "Antworten von allen momentan gefolgten Benutz
confirmShowRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern in der Chronik anzeigen?" confirmShowRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern in der Chronik anzeigen?"
confirmHideRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern nicht in der Chronik anzeigen?" confirmHideRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern nicht in der Chronik anzeigen?"
externalServices: "Externe Dienste" externalServices: "Externe Dienste"
sourceCode: "Quellcode"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "Impressums-URL" impressumUrl: "Impressums-URL"
impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, ist die Angabe von Betreiberinformationen (ein Impressum) bei kommerziellem Betrieb zwingend." impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, ist die Angabe von Betreiberinformationen (ein Impressum) bei kommerziellem Betrieb zwingend."
@ -1183,6 +1184,7 @@ decorate: "Dekorieren"
addMfmFunction: "MFM hinzufügen" addMfmFunction: "MFM hinzufügen"
sfx: "Soundeffekte" sfx: "Soundeffekte"
lastNDays: "Letzten {n} Tage" lastNDays: "Letzten {n} Tage"
surrender: "Abbrechen"
_announcement: _announcement:
forExistingUsers: "Nur für existierende Nutzer" forExistingUsers: "Nur für existierende Nutzer"
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."

View File

@ -991,6 +991,7 @@ neverShow: "Don't show again"
remindMeLater: "Maybe later" remindMeLater: "Maybe later"
didYouLikeMisskey: "Have you taken a liking to Misskey?" didYouLikeMisskey: "Have you taken a liking to Misskey?"
pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!" pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!"
correspondingSourceIsAvailable: "The corresponding source code is available at {anchor}"
roles: "Roles" roles: "Roles"
role: "Role" role: "Role"
noRole: "Role not found" noRole: "Role not found"
@ -1041,6 +1042,8 @@ resetPasswordConfirm: "Really reset your password?"
sensitiveWords: "Sensitive words" sensitiveWords: "Sensitive words"
sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks." sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks."
sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression."
prohibitedWords: "Prohibited words"
prohibitedWordsDescription: "Enables an error when attempting to post a note containing the set word(s). Multiple words can be set, separated by a new line."
prohibitedWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." prohibitedWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression."
hiddenTags: "Hidden hashtags" hiddenTags: "Hidden hashtags"
hiddenTagsDescription: "Select tags which will not shown on trend list.\nMultiple tags could be registered by lines." hiddenTagsDescription: "Select tags which will not shown on trend list.\nMultiple tags could be registered by lines."
@ -1157,6 +1160,7 @@ showRenotes: "Show renotes"
edited: "Edited" edited: "Edited"
notificationRecieveConfig: "Notification Settings" notificationRecieveConfig: "Notification Settings"
mutualFollow: "Mutual follow" mutualFollow: "Mutual follow"
followingOrFollower: "Following or follower"
fileAttachedOnly: "Only notes with files" fileAttachedOnly: "Only notes with files"
showRepliesToOthersInTimeline: "Show replies to others in timeline" showRepliesToOthersInTimeline: "Show replies to others in timeline"
hideRepliesToOthersInTimeline: "Hide replies to others from timeline" hideRepliesToOthersInTimeline: "Hide replies to others from timeline"
@ -1165,6 +1169,13 @@ hideRepliesToOthersInTimelineAll: "Hide replies to others from everyone you foll
confirmShowRepliesAll: "This operation is irreversible. Would you really like to show replies to others from everyone you follow in your timeline?" confirmShowRepliesAll: "This operation is irreversible. Would you really like to show replies to others from everyone you follow in your timeline?"
confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?" confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?"
externalServices: "External Services" externalServices: "External Services"
sourceCode: "Source code"
sourceCodeIsNotYetProvided: "Source code is not yet available. Contact the administrator to fix this problem."
repositoryUrl: "Repository URL"
repositoryUrlDescription: "If you are using Misskey as is (without any changes to the source code), enter https://github.com/misskey-dev/misskey"
repositoryUrlOrTarballRequired: "If you have not published a repository, you must provide a tarball instead. See .config/example.yml for more information."
feedback: "Feedback"
feedbackUrl: "Feedback URL"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "Impressum URL" impressumUrl: "Impressum URL"
impressumDescription: "In some countries, like germany, the inclusion of operator contact information (an Impressum) is legally required for commercial websites." impressumDescription: "In some countries, like germany, the inclusion of operator contact information (an Impressum) is legally required for commercial websites."
@ -1200,6 +1211,8 @@ soundWillBePlayed: "Sound will be played"
showReplay: "View Replay" showReplay: "View Replay"
replay: "Replay" replay: "Replay"
replaying: "Showing replay" replaying: "Showing replay"
endReplay: "Exit Replay"
copyReplayData: "Copy replay data"
ranking: "Ranking" ranking: "Ranking"
lastNDays: "Last {n} days" lastNDays: "Last {n} days"
backToTitle: "Go back to title" backToTitle: "Go back to title"
@ -1207,8 +1220,20 @@ hemisphere: "Where are you located"
withSensitive: "Include notes with sensitive files" withSensitive: "Include notes with sensitive files"
userSaysSomethingSensitive: "Post by {name} contains sensitive content" userSaysSomethingSensitive: "Post by {name} contains sensitive content"
enableHorizontalSwipe: "Swipe to switch tabs" enableHorizontalSwipe: "Swipe to switch tabs"
loading: "Loading"
surrender: "Cancel"
gameRetry: "Retry"
_bubbleGame: _bubbleGame:
howToPlay: "How to play" howToPlay: "How to play"
hold: "Hold"
_score:
score: "Score"
scoreYen: "Amount of money earned"
highScore: "High score"
maxChain: "Maximum number of chains"
yen: "{yen} Yen"
estimatedQty: "{qty} Pieces"
scoreSweets: "{onigiriQtyWithUnit} Onigiri"
_howToPlay: _howToPlay:
section1: "Adjust the position and drop the object into the box." section1: "Adjust the position and drop the object into the box."
section2: "When two objects of the same type touch each other, they will change into a different object and you score points." section2: "When two objects of the same type touch each other, they will change into a different object and you score points."
@ -1630,6 +1655,7 @@ _role:
gtlAvailable: "Can view the global timeline" gtlAvailable: "Can view the global timeline"
ltlAvailable: "Can view the local timeline" ltlAvailable: "Can view the local timeline"
canPublicNote: "Can send public notes" canPublicNote: "Can send public notes"
mentionMax: "Maximum number of mentions in a note"
canInvite: "Can create instance invite codes" canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit" inviteLimit: "Invite limit"
inviteLimitCycle: "Invite limit cooldown" inviteLimitCycle: "Invite limit cooldown"
@ -1653,6 +1679,7 @@ _role:
canUseTranslator: "Translator usage" canUseTranslator: "Translator usage"
avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" avatarDecorationLimit: "Maximum number of avatar decorations that can be applied"
_condition: _condition:
roleAssignedTo: "Assigned to manual roles"
isLocal: "Local user" isLocal: "Local user"
isRemote: "Remote user" isRemote: "Remote user"
createdLessThan: "Less than X has passed since account creation" createdLessThan: "Less than X has passed since account creation"
@ -1753,6 +1780,8 @@ _aboutMisskey:
contributors: "Main contributors" contributors: "Main contributors"
allContributors: "All contributors" allContributors: "All contributors"
source: "Source code" source: "Source code"
original: "Original"
thisIsModifiedVersion: "{name} uses a modified version of the original Misskey."
translation: "Translate Misskey" translation: "Translate Misskey"
donate: "Donate to Misskey" donate: "Donate to Misskey"
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"
@ -1975,8 +2004,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"
@ -1997,6 +2030,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?"
@ -2247,6 +2300,7 @@ _notification:
reactedBySomeUsers: "{n} users reacted" reactedBySomeUsers: "{n} users reacted"
renotedBySomeUsers: "Renote from {n} users" renotedBySomeUsers: "Renote from {n} users"
followedBySomeUsers: "Followed by {n} users" followedBySomeUsers: "Followed by {n} users"
flushNotification: "Clear notifications"
_types: _types:
all: "All" all: "All"
note: "New notes" note: "New notes"
@ -2344,6 +2398,7 @@ _moderationLogTypes:
resetPassword: "Password reset" resetPassword: "Password reset"
suspendRemoteInstance: "Remote instance suspended" suspendRemoteInstance: "Remote instance suspended"
unsuspendRemoteInstance: "Remote instance unsuspended" unsuspendRemoteInstance: "Remote instance unsuspended"
updateRemoteInstanceNote: "Moderation note updated for remote instance."
markSensitiveDriveFile: "File marked as sensitive" markSensitiveDriveFile: "File marked as sensitive"
unmarkSensitiveDriveFile: "File unmarked as sensitive" unmarkSensitiveDriveFile: "File unmarked as sensitive"
resolveAbuseReport: "Report resolved" resolveAbuseReport: "Report resolved"
@ -2464,6 +2519,8 @@ _reversi:
opponentHasSettingsChanged: "The opponent has changed their settings." opponentHasSettingsChanged: "The opponent has changed their settings."
allowIrregularRules: "Irregular rules (completely free)" allowIrregularRules: "Irregular rules (completely free)"
disallowIrregularRules: "No irregular rules" disallowIrregularRules: "No irregular rules"
showBoardLabels: "Display row and column numbering on the board"
useAvatarAsStone: "Turn stones into user avatars"
_offlineScreen: _offlineScreen:
title: "Offline - cannot connect to the server" title: "Offline - cannot connect to the server"
header: "Unable to connect to the server" header: "Unable to connect to the server"

View File

@ -1166,6 +1166,7 @@ hideRepliesToOthersInTimelineAll: "Ocultar tus respuestas a otros usuarios que s
confirmShowRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres mostrar tus respuestas a otros usuarios que sigues en tu línea de tiempo?" confirmShowRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres mostrar tus respuestas a otros usuarios que sigues en tu línea de tiempo?"
confirmHideRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres ocultar tus respuestas a otros usuarios que sigues en tu línea de tiempo?" confirmHideRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres ocultar tus respuestas a otros usuarios que sigues en tu línea de tiempo?"
externalServices: "Servicios Externos" externalServices: "Servicios Externos"
sourceCode: "Código fuente"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "Impressum URL" impressumUrl: "Impressum URL"
impressumDescription: "En algunos países, como Alemania, la inclusión del operador de datos (el Impressum) es requerido legalmente para sitios web comerciales." impressumDescription: "En algunos países, como Alemania, la inclusión del operador de datos (el Impressum) es requerido legalmente para sitios web comerciales."
@ -1208,6 +1209,7 @@ hemisphere: "Región"
withSensitive: "Mostrar notas que contengan material sensible" withSensitive: "Mostrar notas que contengan material sensible"
userSaysSomethingSensitive: "La publicación de {name} contiene material sensible" userSaysSomethingSensitive: "La publicación de {name} contiene material sensible"
enableHorizontalSwipe: "Deslice para cambiar de pestaña" enableHorizontalSwipe: "Deslice para cambiar de pestaña"
surrender: "detener"
_bubbleGame: _bubbleGame:
howToPlay: "Cómo jugar" howToPlay: "Cómo jugar"
_howToPlay: _howToPlay:

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"
@ -380,8 +380,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "Activer hCaptcha" enableHcaptcha: "Activer hCaptcha"
hcaptchaSiteKey: "Clé du site" hcaptchaSiteKey: "Clé du site"
hcaptchaSecretKey: "Clé secrète" hcaptchaSecretKey: "Clé secrète"
mcaptcha: "mCaptcha"
enableMcaptcha: "Activer mCaptcha"
mcaptchaSiteKey: "Clé du site" mcaptchaSiteKey: "Clé du site"
mcaptchaSecretKey: "Clé secrète" mcaptchaSecretKey: "Clé secrète"
mcaptchaInstanceUrl: "URL de l'instance de mCaptcha"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "Activer reCAPTCHA" enableRecaptcha: "Activer reCAPTCHA"
recaptchaSiteKey: "Clé du site" recaptchaSiteKey: "Clé du site"
@ -523,7 +526,7 @@ hideThisNote: "Masquer cette note"
showFeaturedNotesInTimeline: "Afficher les notes des Tendances dans le fil d'actualité" showFeaturedNotesInTimeline: "Afficher les notes des Tendances dans le fil d'actualité"
objectStorage: "Stockage d'objets" objectStorage: "Stockage d'objets"
useObjectStorage: "Utiliser le stockage d'objets" useObjectStorage: "Utiliser le stockage d'objets"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "URL de base"
objectStorageBaseUrlDesc: "Préfixe dURL utilisé pour construire lURL vers le référencement dobjet (média). Spécifiez son URL si vous utilisez un CDN ou un proxy, sinon spécifiez ladresse accessible au public selon le guide de service que vous allez utiliser. P.ex. 'https://<bucket>.s3.amazonaws.com' pour AWS S3 et 'https://storage.googleapis.com/<bucket>' pour GCS." objectStorageBaseUrlDesc: "Préfixe dURL utilisé pour construire lURL vers le référencement dobjet (média). Spécifiez son URL si vous utilisez un CDN ou un proxy, sinon spécifiez ladresse accessible au public selon le guide de service que vous allez utiliser. P.ex. 'https://<bucket>.s3.amazonaws.com' pour AWS S3 et 'https://storage.googleapis.com/<bucket>' pour GCS."
objectStorageBucket: "Bucket" objectStorageBucket: "Bucket"
objectStorageBucketDesc: "Veuillez spécifier le nom du compartiment utilisé sur le service configuré." objectStorageBucketDesc: "Veuillez spécifier le nom du compartiment utilisé sur le service configuré."
@ -628,6 +631,7 @@ medium: "Moyen"
small: "Petit" small: "Petit"
generateAccessToken: "Générer un jeton d'accès" generateAccessToken: "Générer un jeton d'accès"
permission: "Autorisations " permission: "Autorisations "
adminPermission: "Droits de l'administrateur"
enableAll: "Tout activer" enableAll: "Tout activer"
disableAll: "Tout désactiver" disableAll: "Tout désactiver"
tokenRequested: "Autoriser l'accès au compte" tokenRequested: "Autoriser l'accès au compte"
@ -1031,12 +1035,18 @@ nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non sensibles seulement (mentions j'
rolesAssignedToMe: "Rôles attribués à moi" rolesAssignedToMe: "Rôles attribués à moi"
resetPasswordConfirm: "Souhaitez-vous réinitialiser votre mot de passe ?" resetPasswordConfirm: "Souhaitez-vous réinitialiser votre mot de passe ?"
sensitiveWords: "Mots sensibles" sensitiveWords: "Mots sensibles"
sensitiveWordsDescription2: "Séparer par une espace pour créer une expression AND ; entourer de barres obliques pour créer une expression régulière."
prohibitedWords: "Mots interdits"
prohibitedWordsDescription2: "Séparer par une espace pour créer une expression AND ; entourer de barres obliques pour créer une expression régulière."
hiddenTags: "Hashtags cachés" hiddenTags: "Hashtags cachés"
hiddenTagsDescription: "Les hashtags définis ne s'afficheront pas dans les tendances. Vous pouvez définir plusieurs hashtags en faisant un saut de ligne." hiddenTagsDescription: "Les hashtags définis ne s'afficheront pas dans les tendances. Vous pouvez définir plusieurs hashtags en faisant un saut de ligne."
notesSearchNotAvailable: "La recherche de notes n'est pas disponible." notesSearchNotAvailable: "La recherche de notes n'est pas disponible."
license: "Licence" license: "Licence"
unfavoriteConfirm: "Vraiment supprimer des favoris ?"
myClips: "Mes clips" myClips: "Mes clips"
drivecleaner: "Nettoyeur du Disque" drivecleaner: "Nettoyeur du Disque"
retryAllQueuesNow: "Réessayer tous les fils d'attente immédiatement"
retryAllQueuesConfirmTitle: "Vraiment réessayer ?"
retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur." retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur."
enableChartsForRemoteUser: "Générer les graphiques pour les utilisateurs distants" enableChartsForRemoteUser: "Générer les graphiques pour les utilisateurs distants"
enableChartsForFederatedInstances: "Générer les graphiques pour les instances distantes" enableChartsForFederatedInstances: "Générer les graphiques pour les instances distantes"
@ -1046,6 +1056,8 @@ limitWidthOfReaction: "Limiter la largeur maximale des réactions et les affiche
noteIdOrUrl: "Identifiant de la note ou URL" noteIdOrUrl: "Identifiant de la note ou URL"
video: "Vidéo" video: "Vidéo"
videos: "Vidéos" videos: "Vidéos"
audio: "Audio"
audioFiles: "Fichiers audio"
dataSaver: "Économiseur de données" dataSaver: "Économiseur de données"
accountMigration: "Migration de compte" accountMigration: "Migration de compte"
accountMoved: "Cet·te utilisateur·rice a migré son compte vers :" accountMoved: "Cet·te utilisateur·rice a migré son compte vers :"
@ -1084,7 +1096,10 @@ specifyUser: "Spécifier l'utilisateur·rice"
failedToPreviewUrl: "Aperçu d'URL échoué" failedToPreviewUrl: "Aperçu d'URL échoué"
update: "Mettre à jour" update: "Mettre à jour"
rolesThatCanBeUsedThisEmojiAsReaction: "Rôles qui peuvent utiliser cet émoji comme réaction" rolesThatCanBeUsedThisEmojiAsReaction: "Rôles qui peuvent utiliser cet émoji comme réaction"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si aucun rôle n'est spécifié, tout le monde peut utiliser cet émoji comme réaction."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Il faut un rôle public."
cancelReactionConfirm: "Supprimez la réaction ?" cancelReactionConfirm: "Supprimez la réaction ?"
changeReactionConfirm: "Changer la réaction ?"
later: "Plus tard" later: "Plus tard"
goToMisskey: "Retour vers Misskey" goToMisskey: "Retour vers Misskey"
additionalEmojiDictionary: "Dictionnaires d'émojis additionnels" additionalEmojiDictionary: "Dictionnaires d'émojis additionnels"
@ -1110,11 +1125,13 @@ used: "Utilisé"
expired: "Expiré" expired: "Expiré"
doYouAgree: "Êtes-vous daccord ?" doYouAgree: "Êtes-vous daccord ?"
beSureToReadThisAsItIsImportant: "Assurez-vous de le lire; c'est important." beSureToReadThisAsItIsImportant: "Assurez-vous de le lire; c'est important."
iHaveReadXCarefullyAndAgree: "J'ai lu le contenu de « {x} » et donne mon accord."
dialog: "Dialogue" dialog: "Dialogue"
icon: "Avatar" icon: "Avatar"
forYou: "Pour vous" forYou: "Pour vous"
currentAnnouncements: "Annonces actuelles" currentAnnouncements: "Annonces actuelles"
pastAnnouncements: "Annonces passées" pastAnnouncements: "Annonces passées"
youHaveUnreadAnnouncements: "Il y a des annonces non lues."
replies: "Réponses" replies: "Réponses"
renotes: "Renotes" renotes: "Renotes"
loadReplies: "Inclure les réponses" loadReplies: "Inclure les réponses"
@ -1129,6 +1146,7 @@ showRenotes: "Afficher les renotes"
edited: "Modifié" edited: "Modifié"
notificationRecieveConfig: "Paramètres des notifications" notificationRecieveConfig: "Paramètres des notifications"
mutualFollow: "Abonnement mutuel" mutualFollow: "Abonnement mutuel"
fileAttachedOnly: "Avec fichiers joints seulement"
showRepliesToOthersInTimeline: "Afficher les réponses aux autres dans le fil" showRepliesToOthersInTimeline: "Afficher les réponses aux autres dans le fil"
hideRepliesToOthersInTimeline: "Masquer les réponses aux autres dans le fil" hideRepliesToOthersInTimeline: "Masquer les réponses aux autres dans le fil"
showRepliesToOthersInTimelineAll: "Afficher les réponses de toutes les personnes que vous suivez dans le fil" showRepliesToOthersInTimelineAll: "Afficher les réponses de toutes les personnes que vous suivez dans le fil"
@ -1136,6 +1154,12 @@ hideRepliesToOthersInTimelineAll: "Masquer les réponses de toutes les personnes
confirmShowRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment afficher les réponses de toutes les personnes que vous suivez dans le fil ?" confirmShowRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment afficher les réponses de toutes les personnes que vous suivez dans le fil ?"
confirmHideRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment masquer les réponses de toutes les personnes que vous suivez dans le fil ?" confirmHideRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment masquer les réponses de toutes les personnes que vous suivez dans le fil ?"
externalServices: "Services externes" externalServices: "Services externes"
sourceCode: "Code source"
sourceCodeIsNotYetProvided: "Le code source n'est pas encore disponible. Veuillez signaler ce problème aux administrateurs."
repositoryUrl: "URL du dépôt"
repositoryUrlDescription: "Entrez l'URL du dépôt où se trouve le code source ici. Si vous utilisez Misskey tel quel (sans changer le code source), entrez https://github.com/misskey-dev/misskey"
feedback: "Commentaires"
feedbackUrl: "URL pour les commentaires"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "URL de l'impressum" impressumUrl: "URL de l'impressum"
impressumDescription: "Dans certains pays comme l'Allemagne, il est obligatoire d'afficher les informations sur l'opérateur d'un site (un impressum)." impressumDescription: "Dans certains pays comme l'Allemagne, il est obligatoire d'afficher les informations sur l'opérateur d'un site (un impressum)."
@ -1163,7 +1187,32 @@ remainingN: "Restants : {n}"
overwriteContentConfirm: "Voulez-vous remplacer le contenu actuel ?" overwriteContentConfirm: "Voulez-vous remplacer le contenu actuel ?"
seasonalScreenEffect: "Effet d'écran saisonnier" seasonalScreenEffect: "Effet d'écran saisonnier"
decorate: "Décorer" decorate: "Décorer"
addMfmFunction: "Insérer MFM"
enableQuickAddMfmFunction: "Afficher le sélecteur de MFM avancé"
bubbleGame: "Jeu de bulles"
sfx: "Effets sonores"
soundWillBePlayed: "Le son sera joué"
showReplay: "Voir le replay"
replay: "Rediffusion"
replaying: "En cours de rediffusion"
endReplay: "Arrêter la rediffusion"
copyReplayData: "Copier les données de la rediffusion"
ranking: "Classement"
lastNDays: "Derniers {n} jours" lastNDays: "Derniers {n} jours"
backToTitle: "Retourner au titre"
hemisphere: "Votre région"
enableHorizontalSwipe: "Glisser pour changer d'onglet"
loading: "Chargement en cours"
surrender: "Annuler"
gameRetry: "Réessayer"
_bubbleGame:
howToPlay: "Comment jouer"
hold: "Réserver"
_score:
score: "Score"
scoreYen: "Montant gagné"
highScore: "Meilleur score"
yen: "{yen} yens"
_announcement: _announcement:
forExistingUsers: "Pour les utilisateurs existants seulement" forExistingUsers: "Pour les utilisateurs existants seulement"
readConfirmTitle: "Marquer comme lu ?" readConfirmTitle: "Marquer comme lu ?"
@ -1175,7 +1224,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:
@ -1301,10 +1350,13 @@ _achievements:
title: "Régulier III" title: "Régulier III"
description: "Se connecter pour un total de 400 jours" description: "Se connecter pour un total de 400 jours"
_login500: _login500:
title: "Expert I"
description: "Se connecter pour un total de 500 jours" description: "Se connecter pour un total de 500 jours"
_login600: _login600:
title: "Expert II"
description: "Se connecter pour un total de 600 jours" description: "Se connecter pour un total de 600 jours"
_login700: _login700:
title: "Expert III"
description: "Se connecter pour un total de 700 jours" description: "Se connecter pour un total de 700 jours"
_login800: _login800:
description: "Se connecter pour un total de 800 jours" description: "Se connecter pour un total de 800 jours"
@ -1399,9 +1451,12 @@ _role:
description: "Description du rôle" description: "Description du rôle"
permission: "Rôle et autorisations" permission: "Rôle et autorisations"
assignTarget: "Attribuer" assignTarget: "Attribuer"
manual: "Manuel"
manualRoles: "Rôles manuels" manualRoles: "Rôles manuels"
conditional: "Conditionnel"
conditionalRoles: "Rôles conditionnels" conditionalRoles: "Rôles conditionnels"
condition: "Condition" condition: "Condition"
isConditionalRole: "Ceci est un rôle conditionnel."
isPublic: "Rôle public" isPublic: "Rôle public"
options: "Options" options: "Options"
policies: "Stratégies" policies: "Stratégies"

View File

@ -81,7 +81,7 @@ exportRequested: "Kamu telah meminta ekspor. Ini akan memakan waktu sesaat. Sete
importRequested: "Kamu telah meminta impor. Ini akan memakan waktu sesaat." importRequested: "Kamu telah meminta impor. Ini akan memakan waktu sesaat."
lists: "Daftar" lists: "Daftar"
noLists: "Kamu tidak memiliki daftar apapun" noLists: "Kamu tidak memiliki daftar apapun"
note: "Catat" note: "Catatan"
notes: "Catatan" notes: "Catatan"
following: "Ikuti" following: "Ikuti"
followers: "Pengikut" followers: "Pengikut"
@ -381,8 +381,10 @@ enableHcaptcha: "Nyalakan hCaptcha"
hcaptchaSiteKey: "Site Key" hcaptchaSiteKey: "Site Key"
hcaptchaSecretKey: "Secret Key" hcaptchaSecretKey: "Secret Key"
mcaptcha: "mCaptcha" mcaptcha: "mCaptcha"
enableMcaptcha: "Nyalakan mCaptcha"
mcaptchaSiteKey: "Site key" mcaptchaSiteKey: "Site key"
mcaptchaSecretKey: "Secret Key" mcaptchaSecretKey: "Secret Key"
mcaptchaInstanceUrl: "URL instansi mCaptcha"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "Nyalakan reCAPTCHA" enableRecaptcha: "Nyalakan reCAPTCHA"
recaptchaSiteKey: "Site key" recaptchaSiteKey: "Site key"
@ -630,6 +632,7 @@ medium: "Sedang"
small: "Kecil" small: "Kecil"
generateAccessToken: "Buat token akses" generateAccessToken: "Buat token akses"
permission: "Izin" permission: "Izin"
adminPermission: "Wewenang Izin Admin"
enableAll: "Aktifkan semua" enableAll: "Aktifkan semua"
disableAll: "Nonaktifkan semua" disableAll: "Nonaktifkan semua"
tokenRequested: "Berikan ijin akses ke akun" tokenRequested: "Berikan ijin akses ke akun"
@ -1038,6 +1041,7 @@ resetPasswordConfirm: "Yakin untuk mereset kata sandimu?"
sensitiveWords: "Kata sensitif" sensitiveWords: "Kata sensitif"
sensitiveWordsDescription: "Visibilitas dari semua catatan mengandung kata yang telah diatur akan dijadikan \"Beranda\" secara otomatis. Kamu dapat mendaftarkan kata tersebut lebih dari satu dengan menuliskannya di baris baru." sensitiveWordsDescription: "Visibilitas dari semua catatan mengandung kata yang telah diatur akan dijadikan \"Beranda\" secara otomatis. Kamu dapat mendaftarkan kata tersebut lebih dari satu dengan menuliskannya di baris baru."
sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler."
prohibitedWords: "Kata yang dilarang"
prohibitedWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." prohibitedWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler."
hiddenTags: "Tagar tersembunyi" hiddenTags: "Tagar tersembunyi"
hiddenTagsDescription: "Pilih tanda yang mana akan tidak diperlihatkan dalam daftar tren.\nTanda lebih dari satu dapat didaftarkan dengan tiap baris." hiddenTagsDescription: "Pilih tanda yang mana akan tidak diperlihatkan dalam daftar tren.\nTanda lebih dari satu dapat didaftarkan dengan tiap baris."
@ -1057,6 +1061,8 @@ limitWidthOfReaction: "Batasi lebar maksimum reaksi dan tampilkan dalam ukuran t
noteIdOrUrl: "ID catatan atau URL" noteIdOrUrl: "ID catatan atau URL"
video: "Video" video: "Video"
videos: "Video" videos: "Video"
audio: "Suara"
audioFiles: "Berkas Suara"
dataSaver: "Penghemat data" dataSaver: "Penghemat data"
accountMigration: "Pemindahan akun" accountMigration: "Pemindahan akun"
accountMoved: "Pengguna ini telah berpindah ke akun baru:" accountMoved: "Pengguna ini telah berpindah ke akun baru:"
@ -1160,6 +1166,7 @@ hideRepliesToOthersInTimelineAll: "Sembuyikan balasan ke lainnya dari semua oran
confirmShowRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menampilkan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?" confirmShowRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menampilkan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?"
confirmHideRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menyembunyikan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?" confirmHideRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menyembunyikan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?"
externalServices: "Layanan eksternal" externalServices: "Layanan eksternal"
sourceCode: "Sumber kode"
impressum: "Impressum" impressum: "Impressum"
impressumUrl: "Tautan Impressum" impressumUrl: "Tautan Impressum"
impressumDescription: "Pada beberapa negara seperti Jerman, inklusi dari informasi kontak operator (sebuah Impressum) diperlukan secara legal untuk situs web komersil." impressumDescription: "Pada beberapa negara seperti Jerman, inklusi dari informasi kontak operator (sebuah Impressum) diperlukan secara legal untuk situs web komersil."
@ -1191,10 +1198,22 @@ addMfmFunction: "Tambahkan dekorasi"
enableQuickAddMfmFunction: "Tampilkan pemilih MFM tingkat lanjut" enableQuickAddMfmFunction: "Tampilkan pemilih MFM tingkat lanjut"
bubbleGame: "Bubble Game" bubbleGame: "Bubble Game"
sfx: "Efek Suara" sfx: "Efek Suara"
soundWillBePlayed: "Suara yang akan dimainkan"
showReplay: "Lihat tayangan ulang"
replay: "Tayangan ulang"
replaying: "Menayangkan Ulang"
ranking: "Peringkat"
lastNDays: "{n} hari terakhir" lastNDays: "{n} hari terakhir"
backToTitle: "Ke Judul" backToTitle: "Ke Judul"
hemisphere: "Letak kamu tinggal"
withSensitive: "Lampirkan catatan dengan berkas sensitif"
userSaysSomethingSensitive: "Postingan oleh {name} mengandung konten sensitif"
enableHorizontalSwipe: "Geser untuk mengganti tab"
surrender: "Batalkan"
_bubbleGame: _bubbleGame:
howToPlay: "Cara bermain" howToPlay: "Cara bermain"
_howToPlay:
section1: "Atur posisi dan jatuhkan obyek ke dalam kotak."
_announcement: _announcement:
forExistingUsers: "Hanya pengguna yang telah ada" forExistingUsers: "Hanya pengguna yang telah ada"
forExistingUsersDescription: "Pengumuman ini akan dimunculkan ke pengguna yang sudah ada dari titik waktu publikasi jika dinyalakan. Apabila dimatikan, mereka yang baru mendaftar setelah publikasi ini akan juga melihatnya." forExistingUsersDescription: "Pengumuman ini akan dimunculkan ke pengguna yang sudah ada dari titik waktu publikasi jika dinyalakan. Apabila dimatikan, mereka yang baru mendaftar setelah publikasi ini akan juga melihatnya."
@ -1256,6 +1275,8 @@ _initialTutorial:
note: "Baru aja makan donat berlapis coklat 🍩😋" note: "Baru aja makan donat berlapis coklat 🍩😋"
_howToMakeAttachmentsSensitive: _howToMakeAttachmentsSensitive:
title: "Bagaimana menandai lampiran sebagai sensitif?" title: "Bagaimana menandai lampiran sebagai sensitif?"
_done:
title: "Kamu telah menyelesaikan tutorial! 🎉"
_serverRules: _serverRules:
description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan." description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan."
_serverSettings: _serverSettings:
@ -1900,6 +1921,55 @@ _permissions:
"write:flash": "Sunting Play" "write:flash": "Sunting Play"
"read:flash-likes": "Lihat daftar Play yang disukai" "read:flash-likes": "Lihat daftar Play yang disukai"
"write:flash-likes": "Sunting daftar Play yang disukai" "write:flash-likes": "Sunting daftar Play yang disukai"
"read:admin:abuse-user-reports": "Lihat laporan pengguna"
"write:admin:delete-account": "Hapus akun pengguna"
"write:admin:delete-all-files-of-a-user": "Hapus semua berkas dari seorang pengguna"
"read:admin:index-stats": "Lihat statistik indeks basis data"
"read:admin:table-stats": "Lihat statistik tabel basis data"
"read:admin:user-ips": "Lihat alamat IP pengguna"
"read:admin:meta": "Lihat metadata instansi"
"write:admin:reset-password": "Atur ulang kata sandi pengguna"
"write:admin:resolve-abuse-user-report": "Selesaikan laporan pengguna"
"write:admin:send-email": "Mengirim surel"
"read:admin:server-info": "Lihat informasi peladen"
"read:admin:show-moderation-log": "Lihat log moderasi"
"read:admin:show-user": "Lihat informasi pengguna privat"
"read:admin:show-users": "Lihat informasi pengguna privat"
"write:admin:suspend-user": "Tangguhkan pengguna"
"write:admin:unset-user-avatar": "Hapus avatar pengguna"
"write:admin:unset-user-banner": "Hapus banner pengguna"
"write:admin:unsuspend-user": "Batalkan penangguhan pengguna"
"write:admin:meta": "Kelola metadata instansi"
"write:admin:user-note": "Kelola moderasi catatan"
"write:admin:roles": "Kelola peran"
"read:admin:roles": "Lihat peran"
"write:admin:relays": "Kelola relay"
"read:admin:relays": "Lihat relay"
"write:admin:invite-codes": "Kelola kode undangan"
"read:admin:invite-codes": "Lihat kode undangan"
"write:admin:announcements": "Kelola pengumuman"
"read:admin:announcements": "Lihat Pengumuman"
"write:admin:avatar-decorations": "Kelola dekorasi avatar"
"read:admin:avatar-decorations": "Lihat dekorasi avatar"
"write:admin:federation": "Kelola data federasi"
"write:admin:account": "Kelola akun pengguna"
"read:admin:account": "Lihat akun pengguna"
"write:admin:emoji": "Kelola emoji"
"read:admin:emoji": "Lihat emoji"
"write:admin:queue": "Kelola antrian kerja"
"read:admin:queue": "Lihat informasi antrian kerja"
"write:admin:promo": "Kelola catatan promosi"
"write:admin:drive": "Kelola drive pengguna"
"read:admin:drive": "Kelola informasi drive pengguna"
"read:admin:stream": "Gunakan API WebSocket untuk Admin"
"write:admin:ad": "Kelola iklan"
"read:admin:ad": "Lihat iklan"
"write:invite-codes": "Membuat kode undangan"
"read:invite-codes": "Mendapatkan kode undangan"
"write:clip-favorite": "Kelola klip yang difavoritkan"
"read:clip-favorite": "Lihat klip yang difavoritkan"
"read:federation": "Mendapatkan data federasi"
"write:report-abuse": "Melaporkan pelanggaran"
_auth: _auth:
shareAccessTitle: "Mendapatkan ijin akses aplikasi" shareAccessTitle: "Mendapatkan ijin akses aplikasi"
shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?" shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?"
@ -1954,6 +2024,7 @@ _widgets:
_userList: _userList:
chooseList: "Pilih daftar" chooseList: "Pilih daftar"
clicker: "Pengeklik" clicker: "Pengeklik"
birthdayFollowings: "Pengguna yang merayakan hari ulang tahunnya hari ini"
_cw: _cw:
hide: "Sembunyikan" hide: "Sembunyikan"
show: "Lihat konten" show: "Lihat konten"
@ -2320,6 +2391,41 @@ _dataSaver:
_code: _code:
title: "Penyorotan kode" title: "Penyorotan kode"
description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data." description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data."
_hemisphere:
N: "Bumi belahan utara"
S: "Bumi belahan selatan"
caption: "Digunakan dalam beberapa pengaturan klien untuk menentukan musim."
_reversi: _reversi:
reversi: "Reversi"
gameSettings: "Pengaturan permainan"
chooseBoard: "Pilih papan"
blackOrWhite: "Hitam/Putih"
blackIs: "{name} bermain sebagai Hitam"
rules: "Aturan"
thisGameIsStartedSoon: "Permainan akan segera dimulai"
waitingForOther: "Menunggu langkah giliran dari lawan"
waitingForMe: "Menungguh langkah giliran dari kamu"
waitingBoth: "Bersiap"
ready: "Siap"
cancelReady: "Belum siap"
opponentTurn: "Giliran lawan"
myTurn: "Giliran kamu"
turnOf: "Giliran {name}"
pastTurnOf: "Giliran {name}"
surrender: "Menyerah"
surrendered: "Telah menyerah"
timeout: "Waktu habis"
drawn: "Seri"
won: "{name} menang"
black: "Hitam"
white: "Putih"
total: "Jumlah" total: "Jumlah"
turnCount: "Langkah ke {count}"
myGames: "Rondeku"
allGames: "Semua ronde"
ended: "Selesai"
playing: "Sedang bermain"
isLlotheo: "Pemain dengan batu yang sedikit menang (Llotheo)"
loopedMap: "Peta melingkar"
canPutEverywhere: "Keping dapat ditaruh dimana saja"

124
locales/index.d.ts vendored
View File

@ -3980,6 +3980,10 @@ export interface Locale extends ILocale {
* Misskeyは{host}使 * Misskeyは{host}使
*/ */
"pleaseDonate": ParameterizedString<"host">; "pleaseDonate": ParameterizedString<"host">;
/**
* {anchor}
*/
"correspondingSourceIsAvailable": ParameterizedString<"anchor">;
/** /**
* *
*/ */
@ -4652,6 +4656,10 @@ export interface Locale extends ILocale {
* *
*/ */
"mutualFollow": string; "mutualFollow": string;
/**
*
*/
"followingOrFollower": string;
/** /**
* *
*/ */
@ -4684,6 +4692,34 @@ export interface Locale extends ILocale {
* *
*/ */
"externalServices": string; "externalServices": string;
/**
*
*/
"sourceCode": string;
/**
*
*/
"sourceCodeIsNotYetProvided": string;
/**
* URL
*/
"repositoryUrl": string;
/**
* URLを記入しますMisskeyを現状のまま使 https://github.com/misskey-dev/misskey と記入します。
*/
"repositoryUrlDescription": string;
/**
* tarballを提供する必要があります.config/example.ymlを参照してください
*/
"repositoryUrlOrTarballRequired": string;
/**
*
*/
"feedback": string;
/**
* URL
*/
"feedbackUrl": string;
/** /**
* *
*/ */
@ -4824,6 +4860,14 @@ export interface Locale extends ILocale {
* *
*/ */
"replaying": string; "replaying": string;
/**
*
*/
"endReplay": string;
/**
*
*/
"copyReplayData": string;
/** /**
* *
*/ */
@ -4852,11 +4896,57 @@ export interface Locale extends ILocale {
* *
*/ */
"enableHorizontalSwipe": string; "enableHorizontalSwipe": string;
/**
*
*/
"loading": string;
/**
*
*/
"surrender": string;
/**
*
*/
"gameRetry": string;
"_bubbleGame": { "_bubbleGame": {
/** /**
* *
*/ */
"howToPlay": string; "howToPlay": string;
/**
*
*/
"hold": string;
"_score": {
/**
*
*/
"score": string;
/**
*
*/
"scoreYen": string;
/**
*
*/
"highScore": string;
/**
*
*/
"maxChain": string;
/**
* {yen}
*/
"yen": ParameterizedString<"yen">;
/**
* {qty}
*/
"estimatedQty": ParameterizedString<"qty">;
/**
* {onigiriQtyWithUnit}
*/
"scoreSweets": ParameterizedString<"onigiriQtyWithUnit">;
};
"_howToPlay": { "_howToPlay": {
/** /**
* 調 * 調
@ -6352,6 +6442,10 @@ export interface Locale extends ILocale {
* 稿 * 稿
*/ */
"canPublicNote": string; "canPublicNote": string;
/**
*
*/
"mentionMax": string;
/** /**
* *
*/ */
@ -6442,6 +6536,10 @@ export interface Locale extends ILocale {
"avatarDecorationLimit": string; "avatarDecorationLimit": string;
}; };
"_condition": { "_condition": {
/**
*
*/
"roleAssignedTo": string;
/** /**
* *
*/ */
@ -6813,6 +6911,14 @@ export interface Locale extends ILocale {
* *
*/ */
"source": string; "source": string;
/**
*
*/
"original": string;
/**
* {name}Misskeyを改変したバージョンを使用しています
*/
"thisIsModifiedVersion": ParameterizedString<"name">;
/** /**
* Misskeyを翻訳 * Misskeyを翻訳
*/ */
@ -8811,6 +8917,10 @@ export interface Locale extends ILocale {
* {n} * {n}
*/ */
"followedBySomeUsers": ParameterizedString<"n">; "followedBySomeUsers": ParameterizedString<"n">;
/**
*
*/
"flushNotification": string;
"_types": { "_types": {
/** /**
* *
@ -9132,7 +9242,7 @@ export interface Locale extends ILocale {
*/ */
"updateServerSettings": string; "updateServerSettings": string;
/** /**
* *
*/ */
"updateUserNote": string; "updateUserNote": string;
/** /**
@ -9179,6 +9289,10 @@ export interface Locale extends ILocale {
* *
*/ */
"unsuspendRemoteInstance": string; "unsuspendRemoteInstance": string;
/**
*
*/
"updateRemoteInstanceNote": string;
/** /**
* *
*/ */
@ -9615,6 +9729,14 @@ export interface Locale extends ILocale {
* *
*/ */
"disallowIrregularRules": string; "disallowIrregularRules": string;
/**
*
*/
"showBoardLabels": string;
/**
*
*/
"useAvatarAsStone": string;
}; };
"_offlineScreen": { "_offlineScreen": {
/** /**

View File

@ -991,6 +991,7 @@ neverShow: "Non mostrare più"
remindMeLater: "Rimanda" remindMeLater: "Rimanda"
didYouLikeMisskey: "Ti piace Misskey?" didYouLikeMisskey: "Ti piace Misskey?"
pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!" pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!"
correspondingSourceIsAvailable: ""
roles: "Ruoli" roles: "Ruoli"
role: "Ruolo" role: "Ruolo"
noRole: "Ruolo non trovato" noRole: "Ruolo non trovato"
@ -1167,6 +1168,13 @@ hideRepliesToOthersInTimelineAll: "Nascondi le risposte dei tuoi follow nella TL
confirmShowRepliesAll: "Questa è una attività irreversibile. Vuoi davvero includere tutte le risposte dei following in TL?" confirmShowRepliesAll: "Questa è una attività irreversibile. Vuoi davvero includere tutte le risposte dei following in TL?"
confirmHideRepliesAll: "Questa è una attività irreversibile. Vuoi davvero escludere tutte le risposte dei following in TL?" confirmHideRepliesAll: "Questa è una attività irreversibile. Vuoi davvero escludere tutte le risposte dei following in TL?"
externalServices: "Servizi esterni" externalServices: "Servizi esterni"
sourceCode: "Codice sorgente"
sourceCodeIsNotYetProvided: ""
repositoryUrl: "URL della repository"
repositoryUrlDescription: "Se esiste un repository il cui il codice sorgente è disponibile pubblicamente, inserisci il suo URL. Se stai utilizzando Misskey così com'è (senza alcuna modifica al codice sorgente), inserisci https://github.com/misskey-dev/misskey."
repositoryUrlOrTarballRequired: "Se non disponi di un repository pubblico, dovrai fornire un file tarball (tar). Vedere .config/example.yml per i dettagli."
feedback: "Feedback"
feedbackUrl: "URL di feedback"
impressum: "Dichiarazione di proprietà" impressum: "Dichiarazione di proprietà"
impressumUrl: "URL della dichiarazione di proprietà" impressumUrl: "URL della dichiarazione di proprietà"
impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)." impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)."
@ -1198,7 +1206,7 @@ addMfmFunction: "Aggiungi decorazioni"
enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFM" enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFM"
bubbleGame: "Bubble Game" bubbleGame: "Bubble Game"
sfx: "Effetti sonori" sfx: "Effetti sonori"
soundWillBePlayed: "Verrà riprodotto il suono" soundWillBePlayed: "Con musica ed effetti sonori"
showReplay: "Vedi i replay" showReplay: "Vedi i replay"
replay: "Replay" replay: "Replay"
replaying: "Replay in corso" replaying: "Replay in corso"
@ -1209,12 +1217,13 @@ hemisphere: "Geolocalizzazione"
withSensitive: "Mostra le Note con allegati espliciti" withSensitive: "Mostra le Note con allegati espliciti"
userSaysSomethingSensitive: "Note da {name} con allegati espliciti" userSaysSomethingSensitive: "Note da {name} con allegati espliciti"
enableHorizontalSwipe: "Trascina per invertire i tab" enableHorizontalSwipe: "Trascina per invertire i tab"
surrender: "Annulla"
_bubbleGame: _bubbleGame:
howToPlay: "Come giocare" howToPlay: "Come giocare"
_howToPlay: _howToPlay:
section1: "Regola la posizione e rilascia l'oggetto nella casella." section1: "Scegli la posizione e rilascia l'oggetto nel contenitore."
section2: "Ottieni un punteggio, quando due oggetti dello stesso tipo si toccano e si trasformano in un oggetto diverso." section2: "Se due oggetti dello stesso tipo si toccano, si trasformano in un oggetto diverso, aumentando il punteggio."
section3: "Se gli oggetti traboccano dalla scatola, il gioco finisce. Cerca di ottenere un punteggio elevato fondendo gli oggetti, evitando che escano dalla scatola!" section3: "Se gli oggetti escono dal limite superiore del contenitore, il gioco finisce. Cerca di ottenere un punteggio elevato fondendo gli oggetti, evitando che escano dal contenitore!"
_announcement: _announcement:
forExistingUsers: "Solo ai profili attuali" forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
@ -1755,6 +1764,8 @@ _aboutMisskey:
contributors: "Principali sostenitori" contributors: "Principali sostenitori"
allContributors: "Tutti i sostenitori" allContributors: "Tutti i sostenitori"
source: "Codice sorgente" source: "Codice sorgente"
original: "Originale"
thisIsModifiedVersion: "{name} sta usando una versione modificata diversa da Misskey originale."
translation: "Tradurre Misskey" translation: "Tradurre Misskey"
donate: "Sostieni Misskey" donate: "Sostieni Misskey"
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰" morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"

View File

@ -991,6 +991,7 @@ neverShow: "今後表示しない"
remindMeLater: "また後で" remindMeLater: "また後で"
didYouLikeMisskey: "Misskeyを気に入っていただけましたか" didYouLikeMisskey: "Misskeyを気に入っていただけましたか"
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!" pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
correspondingSourceIsAvailable: "対応するソースコードは{anchor}から利用可能です。"
roles: "ロール" roles: "ロール"
role: "ロール" role: "ロール"
noRole: "ロールはありません" noRole: "ロールはありません"
@ -1159,6 +1160,7 @@ showRenotes: "リノートを表示"
edited: "編集済み" edited: "編集済み"
notificationRecieveConfig: "通知の受信設定" notificationRecieveConfig: "通知の受信設定"
mutualFollow: "相互フォロー" mutualFollow: "相互フォロー"
followingOrFollower: "フォロー中またはフォロワー"
fileAttachedOnly: "ファイル付きのみ" fileAttachedOnly: "ファイル付きのみ"
showRepliesToOthersInTimeline: "TLに他の人への返信を含める" showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
@ -1167,6 +1169,13 @@ hideRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返
confirmShowRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか" confirmShowRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか"
confirmHideRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか" confirmHideRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか"
externalServices: "外部サービス" externalServices: "外部サービス"
sourceCode: "ソースコード"
sourceCodeIsNotYetProvided: "ソースコードはまだ提供されていません。この問題の修正について管理者に問い合わせてください。"
repositoryUrl: "リポジトリURL"
repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入します。Misskeyを現状のままソースコードにいかなる変更も加えずに使用している場合は https://github.com/misskey-dev/misskey と記入します。"
repositoryUrlOrTarballRequired: "リポジトリを公開していない場合、代わりにtarballを提供する必要があります。詳細は.config/example.ymlを参照してください。"
feedback: "フィードバック"
feedbackUrl: "フィードバックURL"
impressum: "運営者情報" impressum: "運営者情報"
impressumUrl: "運営者情報URL" impressumUrl: "運営者情報URL"
impressumDescription: "ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。" impressumDescription: "ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。"
@ -1202,6 +1211,8 @@ soundWillBePlayed: "サウンドが再生されます"
showReplay: "リプレイを見る" showReplay: "リプレイを見る"
replay: "リプレイ" replay: "リプレイ"
replaying: "リプレイ中" replaying: "リプレイ中"
endReplay: "リプレイを終了"
copyReplayData: "リプレイデータをコピー"
ranking: "ランキング" ranking: "ランキング"
lastNDays: "直近{n}日" lastNDays: "直近{n}日"
backToTitle: "タイトルへ" backToTitle: "タイトルへ"
@ -1209,9 +1220,21 @@ hemisphere: "お住まいの地域"
withSensitive: "センシティブなファイルを含むノートを表示" withSensitive: "センシティブなファイルを含むノートを表示"
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
enableHorizontalSwipe: "スワイプしてタブを切り替える" enableHorizontalSwipe: "スワイプしてタブを切り替える"
loading: "読み込み中"
surrender: "やめる"
gameRetry: "リトライ"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"
hold: "ホールド"
_score:
score: "スコア"
scoreYen: "稼いだ金額"
highScore: "ハイスコア"
maxChain: "最大チェーン数"
yen: "{yen}円"
estimatedQty: "{qty}個分"
scoreSweets: "おにぎり {onigiriQtyWithUnit}"
_howToPlay: _howToPlay:
section1: "位置を調整してハコにモノを落とします。" section1: "位置を調整してハコにモノを落とします。"
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。" section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
@ -1642,6 +1665,7 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧" gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可" canPublicNote: "パブリック投稿の許可"
mentionMax: "ノート内の最大メンション数"
canInvite: "サーバー招待コードの発行" canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数" inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔" inviteLimitCycle: "招待コードの発行間隔"
@ -1665,6 +1689,7 @@ _role:
canUseTranslator: "翻訳機能の利用" canUseTranslator: "翻訳機能の利用"
avatarDecorationLimit: "アイコンデコレーションの最大取付個数" avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
_condition: _condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
createdLessThan: "アカウント作成から~以内" createdLessThan: "アカウント作成から~以内"
@ -1778,6 +1803,8 @@ _aboutMisskey:
contributors: "コントリビューター" contributors: "コントリビューター"
allContributors: "全てのコントリビューター" allContributors: "全てのコントリビューター"
source: "ソースコード" source: "ソースコード"
original: "オリジナル"
thisIsModifiedVersion: "{name}はオリジナルのMisskeyを改変したバージョンを使用しています。"
translation: "Misskeyを翻訳" translation: "Misskeyを翻訳"
donate: "Misskeyに寄付" donate: "Misskeyに寄付"
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
@ -2330,6 +2357,7 @@ _notification:
reactedBySomeUsers: "{n}人がリアクションしました" reactedBySomeUsers: "{n}人がリアクションしました"
renotedBySomeUsers: "{n}人がリノートしました" renotedBySomeUsers: "{n}人がリノートしました"
followedBySomeUsers: "{n}人にフォローされました" followedBySomeUsers: "{n}人にフォローされました"
flushNotification: "通知の履歴をリセットする"
_types: _types:
all: "すべて" all: "すべて"
@ -2424,7 +2452,7 @@ _moderationLogTypes:
updateCustomEmoji: "カスタム絵文字更新" updateCustomEmoji: "カスタム絵文字更新"
deleteCustomEmoji: "カスタム絵文字削除" deleteCustomEmoji: "カスタム絵文字削除"
updateServerSettings: "サーバー設定更新" updateServerSettings: "サーバー設定更新"
updateUserNote: "モデレーションノート更新" updateUserNote: "ユーザーのモデレーションノート更新"
deleteDriveFile: "ファイルを削除" deleteDriveFile: "ファイルを削除"
deleteNote: "ノートを削除" deleteNote: "ノートを削除"
createGlobalAnnouncement: "全体のお知らせを作成" createGlobalAnnouncement: "全体のお知らせを作成"
@ -2436,6 +2464,7 @@ _moderationLogTypes:
resetPassword: "パスワードをリセット" resetPassword: "パスワードをリセット"
suspendRemoteInstance: "リモートサーバーを停止" suspendRemoteInstance: "リモートサーバーを停止"
unsuspendRemoteInstance: "リモートサーバーを再開" unsuspendRemoteInstance: "リモートサーバーを再開"
updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新"
markSensitiveDriveFile: "ファイルをセンシティブ付与" markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決" resolveAbuseReport: "通報を解決"
@ -2561,6 +2590,8 @@ _reversi:
opponentHasSettingsChanged: "相手が設定を変更しました" opponentHasSettingsChanged: "相手が設定を変更しました"
allowIrregularRules: "変則許可 (完全フリー)" allowIrregularRules: "変則許可 (完全フリー)"
disallowIrregularRules: "変則なし" disallowIrregularRules: "変則なし"
showBoardLabels: "盤面に行・列番号を表示"
useAvatarAsStone: "石をアイコンにする"
_offlineScreen: _offlineScreen:
title: "オフライン - サーバーに接続できません" title: "オフライン - サーバーに接続できません"

View File

@ -991,6 +991,7 @@ neverShow: "今後表示しない"
remindMeLater: "また後で" remindMeLater: "また後で"
didYouLikeMisskey: "Misskey気に入ってくれた" didYouLikeMisskey: "Misskey気に入ってくれた"
pleaseDonate: "Misskeyは{host}が使うとる無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。" pleaseDonate: "Misskeyは{host}が使うとる無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
correspondingSourceIsAvailable: "{anchor}"
roles: "ロール" roles: "ロール"
role: "ロール" role: "ロール"
noRole: "ロールはありまへん" noRole: "ロールはありまへん"
@ -1165,6 +1166,7 @@ hideRepliesToOthersInTimelineAll: "タイムラインに今フォローしとる
confirmShowRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れるか?" confirmShowRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れるか?"
confirmHideRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れへんのか?" confirmHideRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れへんのか?"
externalServices: "他のサイトのサービス" externalServices: "他のサイトのサービス"
sourceCode: "ソースコード"
impressum: "運営者の情報" impressum: "運営者の情報"
impressumUrl: "運営者の情報URL" impressumUrl: "運営者の情報URL"
impressumDescription: "ドイツとかの一部んところではな、表示が義務付けられてんねん(Impressum)。" impressumDescription: "ドイツとかの一部んところではな、表示が義務付けられてんねん(Impressum)。"
@ -1207,6 +1209,7 @@ hemisphere: "住んでる地域"
withSensitive: "センシティブなファイルを含むノートを表示" withSensitive: "センシティブなファイルを含むノートを表示"
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
enableHorizontalSwipe: "スワイプしてタブを切り替える" enableHorizontalSwipe: "スワイプしてタブを切り替える"
surrender: "やめとく"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"
_howToPlay: _howToPlay:

View File

@ -640,6 +640,7 @@ icon: "아바타"
replies: "답하기" replies: "답하기"
renotes: "리노트" renotes: "리노트"
attach: "옇기" attach: "옇기"
surrender: "아이예"
_initialAccountSetting: _initialAccountSetting:
startTutorial: "길라잡이 하기" startTutorial: "길라잡이 하기"
_initialTutorial: _initialTutorial:

View File

@ -991,6 +991,7 @@ neverShow: "다시 보지 않기"
remindMeLater: "나중에 알림" remindMeLater: "나중에 알림"
didYouLikeMisskey: "Misskey가 마음에 드시나요?" didYouLikeMisskey: "Misskey가 마음에 드시나요?"
pleaseDonate: "Misskey는 {host} 서버의 무료 소프트웨어입니다. 앞으로도 개발을 이어 나가려면 후원이 절실히 필요합니다!" pleaseDonate: "Misskey는 {host} 서버의 무료 소프트웨어입니다. 앞으로도 개발을 이어 나가려면 후원이 절실히 필요합니다!"
correspondingSourceIsAvailable: "소스 코드는 {anchor}에서 받아보실 수 있습니다."
roles: "역할" roles: "역할"
role: "역할" role: "역할"
noRole: "역할이 없습니다" noRole: "역할이 없습니다"
@ -1041,6 +1042,8 @@ resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
sensitiveWords: "민감한 단어" sensitiveWords: "민감한 단어"
sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다." sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다."
sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
prohibitedWords: "금지 워드"
prohibitedWordsDescription: "설정된 워드가 포함되는 노트를 작성하려고 하면, 에러가 발생하도록 합니다. 줄바꿈으로 구분지어 복수 설정할 수 있습니다."
prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." prohibitedWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다."
hiddenTags: "숨긴 해시태그" hiddenTags: "숨긴 해시태그"
hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다." hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다."
@ -1165,6 +1168,13 @@ hideRepliesToOthersInTimelineAll: "타임라인에 현재 팔로우 중인 사
confirmShowRepliesAll: "이 조작은 되돌릴 수 없습니다. 정말로 타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오게 하시겠습니까?" confirmShowRepliesAll: "이 조작은 되돌릴 수 없습니다. 정말로 타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오게 하시겠습니까?"
confirmHideRepliesAll: "이 조작은 되돌릴 수 없습니다. 정말로 타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오지 않게 하시겠습니까?" confirmHideRepliesAll: "이 조작은 되돌릴 수 없습니다. 정말로 타임라인에 현재 팔로우 중인 사람 전원의 답글이 나오지 않게 하시겠습니까?"
externalServices: "외부 서비스" externalServices: "외부 서비스"
sourceCode: "소스 코드"
sourceCodeIsNotYetProvided: "소스 코드를 아직 제공하지 않습니다. 이 문제를 해결하려면 관리자에게 문의해 주세요."
repositoryUrl: "저장소 URL"
repositoryUrlDescription: "소스 코드를 공개한 저장소가 있는 경우, 그 URL을 적습니다. Misskey를 원본 그대로 (소스 코드를 어떤 식으로도 변경하지 않고) 쓰고 있는 경우 https://github.com/misskey-dev/misskey 라고 적습니다."
repositoryUrlOrTarballRequired: "저장소를 공개하지 않은 경우 대신 tarball을 제공할 필요가 있습니다. 세부사항은 .config/example.yml을 참조해 주세요."
feedback: "피드백"
feedbackUrl: "피드백 URL"
impressum: "운영자 정보" impressum: "운영자 정보"
impressumUrl: "운영자 정보 URL" impressumUrl: "운영자 정보 URL"
impressumDescription: "독일 등의 일부 나라와 지역에서는 꼭 표시해야 합니다(Impressum)." impressumDescription: "독일 등의 일부 나라와 지역에서는 꼭 표시해야 합니다(Impressum)."
@ -1207,6 +1217,7 @@ hemisphere: "거주 지역"
withSensitive: "민감한 파일이 포함된 노트 보기" withSensitive: "민감한 파일이 포함된 노트 보기"
userSaysSomethingSensitive: "{name}의 민감한 파일이 포함된 게시물" userSaysSomethingSensitive: "{name}의 민감한 파일이 포함된 게시물"
enableHorizontalSwipe: "스와이프하여 탭 전환" enableHorizontalSwipe: "스와이프하여 탭 전환"
surrender: "그만두기"
_bubbleGame: _bubbleGame:
howToPlay: "설명" howToPlay: "설명"
_howToPlay: _howToPlay:
@ -1753,6 +1764,8 @@ _aboutMisskey:
contributors: "주요 기여자" contributors: "주요 기여자"
allContributors: "모든 기여자" allContributors: "모든 기여자"
source: "소스 코드" source: "소스 코드"
original: "원본"
thisIsModifiedVersion: "{name}에서는 원본 미스키를 수정한 버전을 사용하고 있습니다."
translation: "Misskey를 번역하기" translation: "Misskey를 번역하기"
donate: "Misskey에 기부하기" donate: "Misskey에 기부하기"
morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰" morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰"
@ -2368,6 +2381,7 @@ _moderationLogTypes:
resetPassword: "비밀번호 재설정" resetPassword: "비밀번호 재설정"
suspendRemoteInstance: "리모트 서버를 정지" suspendRemoteInstance: "리모트 서버를 정지"
unsuspendRemoteInstance: "리모트 서버의 정지를 해제" unsuspendRemoteInstance: "리모트 서버의 정지를 해제"
updateRemoteInstanceNote: "리모트 서버의 조정 기록 갱신"
markSensitiveDriveFile: "파일에 열람주의를 설정" markSensitiveDriveFile: "파일에 열람주의를 설정"
unmarkSensitiveDriveFile: "파일에 열람주의를 해제" unmarkSensitiveDriveFile: "파일에 열람주의를 해제"
resolveAbuseReport: "신고 처리" resolveAbuseReport: "신고 처리"

View File

@ -463,6 +463,7 @@ options: "Alternativ"
icon: "Avatar" icon: "Avatar"
replies: "Svar" replies: "Svar"
renotes: "Renote" renotes: "Renote"
surrender: "Avbryt"
_initialAccountSetting: _initialAccountSetting:
theseSettingsCanEditLater: "Du kan endre disse innstillingene senere." theseSettingsCanEditLater: "Du kan endre disse innstillingene senere."
_achievements: _achievements:

View File

@ -871,6 +871,7 @@ youFollowing: "Śledzeni"
icon: "Awatar" icon: "Awatar"
replies: "Odpowiedz" replies: "Odpowiedz"
renotes: "Udostępnij" renotes: "Udostępnij"
sourceCode: "Kod źródłowy"
flip: "Odwróć" flip: "Odwróć"
_role: _role:
priority: "Priorytet" priority: "Priorytet"

View File

@ -1011,6 +1011,7 @@ renotes: "Repostar"
keepScreenOn: "Manter a tela do dispositivo sempre ligada" keepScreenOn: "Manter a tela do dispositivo sempre ligada"
flip: "Inversão" flip: "Inversão"
lastNDays: "Últimos {n} dias" lastNDays: "Últimos {n} dias"
surrender: "Cancelar"
_initialAccountSetting: _initialAccountSetting:
followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo." followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo."
_serverSettings: _serverSettings:

View File

@ -1082,8 +1082,10 @@ icon: "Аватар"
replies: "Ответы" replies: "Ответы"
renotes: "Репост" renotes: "Репост"
loadReplies: "Показать ответы" loadReplies: "Показать ответы"
sourceCode: "Исходный код"
flip: "Переворот" flip: "Переворот"
lastNDays: "Последние {n} сут" lastNDays: "Последние {n} сут"
surrender: "Этот пост не может быть отменен."
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Аккаунт успешно создан!" accountCreated: "Аккаунт успешно создан!"
letsStartAccountSetup: "Давайте настроим вашу учётную запись." letsStartAccountSetup: "Давайте настроим вашу учётную запись."

View File

@ -919,6 +919,7 @@ youFollowing: "Sledované"
icon: "Avatar" icon: "Avatar"
replies: "Odpovedať" replies: "Odpovedať"
renotes: "Preposlať" renotes: "Preposlať"
sourceCode: "Zdrojový kód"
flip: "Preklopiť" flip: "Preklopiť"
lastNDays: "Posledných {n} dní" lastNDays: "Posledných {n} dní"
_role: _role:

File diff suppressed because it is too large Load Diff

View File

@ -911,6 +911,7 @@ youFollowing: "Підписки"
icon: "Аватар" icon: "Аватар"
replies: "Відповісти" replies: "Відповісти"
renotes: "Поширити" renotes: "Поширити"
sourceCode: "Вихідний код"
flip: "Перевернути" flip: "Перевернути"
lastNDays: "Останні {n} днів" lastNDays: "Останні {n} днів"
_achievements: _achievements:

View File

@ -1045,8 +1045,10 @@ loadReplies: "Hiển thị các trả lời"
pinnedList: "Các mục đã được ghim" pinnedList: "Các mục đã được ghim"
keepScreenOn: "Giữ màn hình luôn bật" keepScreenOn: "Giữ màn hình luôn bật"
verifiedLink: "Chúng tôi đã xác nhận bạn là chủ sở hữu của đường dẫn này" verifiedLink: "Chúng tôi đã xác nhận bạn là chủ sở hữu của đường dẫn này"
sourceCode: "Mã nguồn"
flip: "Lật" flip: "Lật"
lastNDays: "{n} ngày trước" lastNDays: "{n} ngày trước"
surrender: "Từ chối"
_announcement: _announcement:
forExistingUsers: "Chỉ những người dùng đã tồn tại" forExistingUsers: "Chỉ những người dùng đã tồn tại"
forExistingUsersDescription: "Nếu được bật, thông báo này sẽ chỉ hiển thị với những người dùng đã tồn tại vào lúc thông báo được tạo. Nếu tắt đi, những tài khoản mới đăng ký sau khi thông báo được đăng lên cũng sẽ thấy nó." forExistingUsersDescription: "Nếu được bật, thông báo này sẽ chỉ hiển thị với những người dùng đã tồn tại vào lúc thông báo được tạo. Nếu tắt đi, những tài khoản mới đăng ký sau khi thông báo được đăng lên cũng sẽ thấy nó."

View File

@ -11,7 +11,7 @@ password: "密码"
forgotPassword: "忘记密码" forgotPassword: "忘记密码"
fetchingAsApObject: "在联邦宇宙查询中..." fetchingAsApObject: "在联邦宇宙查询中..."
ok: "OK" ok: "OK"
gotIt: "我明白了" gotIt: ""
cancel: "取消" cancel: "取消"
noThankYou: "不用,谢谢" noThankYou: "不用,谢谢"
enterUsername: "输入用户名" enterUsername: "输入用户名"
@ -336,7 +336,7 @@ displayOfSensitiveMedia: "显示敏感媒体"
whenServerDisconnected: "与服务器连接中断时" whenServerDisconnected: "与服务器连接中断时"
disconnectedFromServer: "已和服务器断开连接" disconnectedFromServer: "已和服务器断开连接"
reload: "重新加载" reload: "重新加载"
doNothing: "关闭弹窗" doNothing: "关闭"
reloadConfirm: "确定要重新加载吗?" reloadConfirm: "确定要重新加载吗?"
watch: "关注" watch: "关注"
unwatch: "取消关注" unwatch: "取消关注"
@ -991,6 +991,7 @@ neverShow: "不再显示"
remindMeLater: "稍后提醒我" remindMeLater: "稍后提醒我"
didYouLikeMisskey: "您喜欢 Misskey 吗?" didYouLikeMisskey: "您喜欢 Misskey 吗?"
pleaseDonate: "Misskey 是 {host} 所使用的免费软件。为了今后也能够维持 Misskey 的开发,请在有余力的情况下进行捐助!" pleaseDonate: "Misskey 是 {host} 所使用的免费软件。为了今后也能够维持 Misskey 的开发,请在有余力的情况下进行捐助!"
correspondingSourceIsAvailable: "对应的源代码可在{anchor}找到"
roles: "角色" roles: "角色"
role: "角色" role: "角色"
noRole: "角色不存在" noRole: "角色不存在"
@ -1041,6 +1042,8 @@ resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词" sensitiveWords: "敏感词"
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。" sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
prohibitedWords: "禁用词"
prohibitedWordsDescription: "发布包含设定词汇的帖子时将出错。可用换行设定多个关键字"
prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
hiddenTags: "隐藏标签" hiddenTags: "隐藏标签"
hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。" hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。"
@ -1114,7 +1117,7 @@ branding: "品牌"
enableServerMachineStats: "公开服务器硬件统计信息" enableServerMachineStats: "公开服务器硬件统计信息"
enableIdenticonGeneration: "启用生成用户 Identicon" enableIdenticonGeneration: "启用生成用户 Identicon"
turnOffToImprovePerformance: "关闭该选项可以提高性能。" turnOffToImprovePerformance: "关闭该选项可以提高性能。"
createInviteCode: "发行邀请码" createInviteCode: "生成邀请码"
createWithOptions: "使用选项来创建" createWithOptions: "使用选项来创建"
createCount: "发行数" createCount: "发行数"
inviteCodeCreated: "已创建邀请码" inviteCodeCreated: "已创建邀请码"
@ -1126,7 +1129,7 @@ noExpirationDate: "不设置有效日期"
inviteCodeUsedAt: "邀请码被使用的日期和时间" inviteCodeUsedAt: "邀请码被使用的日期和时间"
registeredUserUsingInviteCode: "使用了邀请码的用户" registeredUserUsingInviteCode: "使用了邀请码的用户"
waitingForMailAuth: "等待验证电子邮件" waitingForMailAuth: "等待验证电子邮件"
inviteCodeCreator: "发行邀请码的用户" inviteCodeCreator: "生成邀请码的用户"
usedAt: "使用时间" usedAt: "使用时间"
unused: "未使用" unused: "未使用"
used: "已使用" used: "已使用"
@ -1157,6 +1160,7 @@ showRenotes: "显示转帖"
edited: "已编辑" edited: "已编辑"
notificationRecieveConfig: "通知接收设置" notificationRecieveConfig: "通知接收设置"
mutualFollow: "互相关注" mutualFollow: "互相关注"
followingOrFollower: "关注中或关注者"
fileAttachedOnly: "仅限媒体" fileAttachedOnly: "仅限媒体"
showRepliesToOthersInTimeline: "在时间线中包含给别人的回复" showRepliesToOthersInTimeline: "在时间线中包含给别人的回复"
hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复" hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复"
@ -1165,6 +1169,13 @@ hideRepliesToOthersInTimelineAll: "在时间线中隐藏现在关注的所有人
confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中包含现在关注的所有人的回复吗?" confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中包含现在关注的所有人的回复吗?"
confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏现在关注的所有人的回复吗?" confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏现在关注的所有人的回复吗?"
externalServices: "外部服务" externalServices: "外部服务"
sourceCode: "源代码"
sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。"
repositoryUrl: "仓库地址"
repositoryUrlDescription: "若源代码所在的仓库是公开的,请填入对应的 URL。若是按原样使用 Misskey并未追加或者修改代码的情况请填入 https://github.com/misskey-dev/misskey。"
repositoryUrlOrTarballRequired: "若仓库并未公开,则需要提供 tarball 作为替代。详情请看 .config/example.yml。"
feedback: "反馈"
feedbackUrl: "反馈地址"
impressum: "运营商信息" impressum: "运营商信息"
impressumUrl: "运营商信息地址" impressumUrl: "运营商信息地址"
impressumDescription: "德国等国家和地区有义务展示此类信息Impressum。" impressumDescription: "德国等国家和地区有义务展示此类信息Impressum。"
@ -1194,11 +1205,14 @@ seasonalScreenEffect: "应景的画面效果"
decorate: "装饰" decorate: "装饰"
addMfmFunction: "添加装饰" addMfmFunction: "添加装饰"
enableQuickAddMfmFunction: "显示高级 MFM 选择器" enableQuickAddMfmFunction: "显示高级 MFM 选择器"
bubbleGame: "泡泡游戏"
sfx: "音效" sfx: "音效"
soundWillBePlayed: "声音将会播放" soundWillBePlayed: "声音将会播放"
showReplay: "查看重播" showReplay: "观看回放"
replay: "重播" replay: "重播"
replaying: "重播中" replaying: "重播中"
endReplay: "结束回放"
copyReplayData: "复制回放数据"
ranking: "排行榜" ranking: "排行榜"
lastNDays: "最近 {n} 天" lastNDays: "最近 {n} 天"
backToTitle: "返回标题" backToTitle: "返回标题"
@ -1206,8 +1220,19 @@ hemisphere: "居住地区"
withSensitive: "显示包含敏感媒体的帖子" withSensitive: "显示包含敏感媒体的帖子"
userSaysSomethingSensitive: "含 {name} 敏感文件的帖子" userSaysSomethingSensitive: "含 {name} 敏感文件的帖子"
enableHorizontalSwipe: "滑动切换标签页" enableHorizontalSwipe: "滑动切换标签页"
loading: "读取中"
surrender: "取消"
gameRetry: "重试"
_bubbleGame: _bubbleGame:
howToPlay: "游戏说明" howToPlay: "游戏说明"
hold: "抓住"
_score:
score: "得分"
scoreYen: "赚到的钱"
highScore: "最高分"
maxChain: "最高连击数"
yen: "{yen} 日元"
estimatedQty: "约 {qty} 个"
_howToPlay: _howToPlay:
section1: "对准位置将Emoji投入盒子。" section1: "对准位置将Emoji投入盒子。"
section2: "相同的Emoji相互接触合成后会得到新的Emoji以此获得分数。" section2: "相同的Emoji相互接触合成后会得到新的Emoji以此获得分数。"
@ -1296,8 +1321,8 @@ _initialTutorial:
description: "对于服务器方针所要求要求的,又或者不适合直接展示的附件,请添加「敏感」标记。\n" description: "对于服务器方针所要求要求的,又或者不适合直接展示的附件,请添加「敏感」标记。\n"
tryThisFile: "试试看,将附加到此窗口的图像标注为敏感!" tryThisFile: "试试看,将附加到此窗口的图像标注为敏感!"
_exampleNote: _exampleNote:
note: "不该打开纳豆的盖子的……" note: "拆纳豆包装时出错了…"
method: "要标注附件为敏感内容,请单击该文件以打开菜单,然后单击“设置为敏感”。" method: "要标注附件为敏感内容,请单击该文件以打开菜单,然后单击“标记为敏感内容”。"
sensitiveSucceeded: "附加文件时,请遵循服务器的条款来设置正确敏感设定。\n" sensitiveSucceeded: "附加文件时,请遵循服务器的条款来设置正确敏感设定。\n"
doItToContinue: "将图像标记为敏感后才能够继续" doItToContinue: "将图像标记为敏感后才能够继续"
_done: _done:
@ -1628,8 +1653,9 @@ _role:
gtlAvailable: "查看全局时间线" gtlAvailable: "查看全局时间线"
ltlAvailable: "查看本地时间线" ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖" canPublicNote: "允许公开发帖"
mentionMax: "帖子内最多提及数"
canInvite: "发放服务器邀请码" canInvite: "发放服务器邀请码"
inviteLimit: "可发行邀请码的数量" inviteLimit: "可生成邀请码的数量"
inviteLimitCycle: "邀请码的发行间隔" inviteLimitCycle: "邀请码的发行间隔"
inviteExpirationTime: "邀请码的有效日期" inviteExpirationTime: "邀请码的有效日期"
canManageCustomEmojis: "管理自定义表情符号" canManageCustomEmojis: "管理自定义表情符号"
@ -1651,6 +1677,7 @@ _role:
canUseTranslator: "使用翻译功能" canUseTranslator: "使用翻译功能"
avatarDecorationLimit: "可添加头像挂件的最大个数" avatarDecorationLimit: "可添加头像挂件的最大个数"
_condition: _condition:
roleAssignedTo: "已分配给手动角色"
isLocal: "是本地用户" isLocal: "是本地用户"
isRemote: "是远程用户" isRemote: "是远程用户"
createdLessThan: "账户创建时间少于" createdLessThan: "账户创建时间少于"
@ -1751,6 +1778,8 @@ _aboutMisskey:
contributors: "主要贡献者" contributors: "主要贡献者"
allContributors: "全体贡献者" allContributors: "全体贡献者"
source: "源代码" source: "源代码"
original: "原版"
thisIsModifiedVersion: "{name}正在使用修改后的 Misskey。"
translation: "翻译 Misskey" translation: "翻译 Misskey"
donate: "赞助 Misskey" donate: "赞助 Misskey"
morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰" morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰"
@ -2013,7 +2042,7 @@ _permissions:
"read:admin:stream": "使用管理员用的 Websocket API" "read:admin:stream": "使用管理员用的 Websocket API"
"write:admin:ad": "编辑广告" "write:admin:ad": "编辑广告"
"read:admin:ad": "查看广告" "read:admin:ad": "查看广告"
"write:invite-codes": "发行邀请码" "write:invite-codes": "生成邀请码"
"read:invite-codes": "获取已发行的邀请码" "read:invite-codes": "获取已发行的邀请码"
"write:clip-favorite": "编辑便签的点赞" "write:clip-favorite": "编辑便签的点赞"
"read:clip-favorite": "查看便签的点赞" "read:clip-favorite": "查看便签的点赞"
@ -2269,6 +2298,7 @@ _notification:
reactedBySomeUsers: "{n} 人回应了" reactedBySomeUsers: "{n} 人回应了"
renotedBySomeUsers: "{n} 人转发了" renotedBySomeUsers: "{n} 人转发了"
followedBySomeUsers: "被 {n} 人关注" followedBySomeUsers: "被 {n} 人关注"
flushNotification: "重置通知历史"
_types: _types:
all: "全部" all: "全部"
note: "用户的新帖子" note: "用户的新帖子"
@ -2366,10 +2396,11 @@ _moderationLogTypes:
resetPassword: "重置密码" resetPassword: "重置密码"
suspendRemoteInstance: "停止远程服务器" suspendRemoteInstance: "停止远程服务器"
unsuspendRemoteInstance: "恢复远程服务器" unsuspendRemoteInstance: "恢复远程服务器"
updateRemoteInstanceNote: "更新远程服务器的管理笔记"
markSensitiveDriveFile: "标记网盘文件为敏感媒体" markSensitiveDriveFile: "标记网盘文件为敏感媒体"
unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体" unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体"
resolveAbuseReport: "处理举报" resolveAbuseReport: "处理举报"
createInvitation: "发行邀请码" createInvitation: "生成邀请码"
createAd: "创建了广告" createAd: "创建了广告"
deleteAd: "删除了广告" deleteAd: "删除了广告"
updateAd: "更新了广告" updateAd: "更新了广告"
@ -2460,6 +2491,8 @@ _reversi:
myTurn: "你的回合" myTurn: "你的回合"
turnOf: "{name}的回合" turnOf: "{name}的回合"
pastTurnOf: "{name}的回合" pastTurnOf: "{name}的回合"
surrender: "认输"
surrendered: "已认输"
timeout: "超时" timeout: "超时"
drawn: "平局" drawn: "平局"
won: "{name}获胜" won: "{name}获胜"
@ -2481,6 +2514,8 @@ _reversi:
opponentHasSettingsChanged: "对手更改了设定" opponentHasSettingsChanged: "对手更改了设定"
allowIrregularRules: "允许非常规规则(完全自由)" allowIrregularRules: "允许非常规规则(完全自由)"
disallowIrregularRules: "禁止非常规规则" disallowIrregularRules: "禁止非常规规则"
showBoardLabels: "显示行号和列号"
useAvatarAsStone: "用头像作为棋子"
_offlineScreen: _offlineScreen:
title: "离线——无法连接到服务器" title: "离线——无法连接到服务器"
header: "无法连接到服务器" header: "无法连接到服务器"

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: "票選活動"
@ -991,6 +991,7 @@ neverShow: "不再顯示"
remindMeLater: "以後再說" remindMeLater: "以後再說"
didYouLikeMisskey: "您喜歡 Misskey 嗎?" didYouLikeMisskey: "您喜歡 Misskey 嗎?"
pleaseDonate: "Misskey 是由 {host} 使用的免費軟體。請贊助我們,讓開發得以持續!" pleaseDonate: "Misskey 是由 {host} 使用的免費軟體。請贊助我們,讓開發得以持續!"
correspondingSourceIsAvailable: "對應的原始碼可以在 {anchor} 處找到。"
roles: "角色" roles: "角色"
role: "角色" role: "角色"
noRole: "沒有角色" noRole: "沒有角色"
@ -1159,6 +1160,7 @@ showRenotes: "顯示其他人的轉發貼文"
edited: "已編輯" edited: "已編輯"
notificationRecieveConfig: "接受通知的設定" notificationRecieveConfig: "接受通知的設定"
mutualFollow: "互相追隨" mutualFollow: "互相追隨"
followingOrFollower: "追隨中或追隨者"
fileAttachedOnly: "顯示包含附件的貼文" fileAttachedOnly: "顯示包含附件的貼文"
showRepliesToOthersInTimeline: "顯示給其他人的回覆" showRepliesToOthersInTimeline: "顯示給其他人的回覆"
hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆" hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
@ -1167,6 +1169,13 @@ hideRepliesToOthersInTimelineAll: "在時間軸不包含追隨中所有人的回
confirmShowRepliesAll: "進行此操作後無法復原。您真的希望時間軸「包含」您目前追隨的所有人的回覆嗎?" confirmShowRepliesAll: "進行此操作後無法復原。您真的希望時間軸「包含」您目前追隨的所有人的回覆嗎?"
confirmHideRepliesAll: "進行此操作後無法復原。您真的希望時間軸「不包含」您目前追隨的所有人的回覆嗎?" confirmHideRepliesAll: "進行此操作後無法復原。您真的希望時間軸「不包含」您目前追隨的所有人的回覆嗎?"
externalServices: "外部服務" externalServices: "外部服務"
sourceCode: "原始碼"
sourceCodeIsNotYetProvided: "尚未提供原始碼,請洽詢管理員解決這個問題。"
repositoryUrl: "儲存庫 URL"
repositoryUrlDescription: "如果存在可公開取得原始碼的儲存庫,請輸入其 URL。 如果您按原樣使用 Misskey不對原始碼進行任何更改請輸入 https://github.com/misskey-dev/misskey。"
repositoryUrlOrTarballRequired: "如果儲存庫不是公開的,則必須提供 tarball。 詳細資訊請參閱 .config/example.yml。"
feedback: "意見回饋"
feedbackUrl: "意見回饋 URL"
impressum: "營運者資訊" impressum: "營運者資訊"
impressumUrl: "營運者資訊網址" impressumUrl: "營運者資訊網址"
impressumDescription: "在德國與部份地區必須要明確顯示營運者資訊。" impressumDescription: "在德國與部份地區必須要明確顯示營運者資訊。"
@ -1202,6 +1211,8 @@ soundWillBePlayed: "將播放音效"
showReplay: "觀看重播" showReplay: "觀看重播"
replay: "重播" replay: "重播"
replaying: "重播中" replaying: "重播中"
endReplay: "退出重播"
copyReplayData: "複製重播資料"
ranking: "排行榜" ranking: "排行榜"
lastNDays: "過去 {n} 天" lastNDays: "過去 {n} 天"
backToTitle: "回到遊戲標題頁" backToTitle: "回到遊戲標題頁"
@ -1209,8 +1220,20 @@ hemisphere: "您居住的地區"
withSensitive: "顯示包含敏感檔案的貼文" withSensitive: "顯示包含敏感檔案的貼文"
userSaysSomethingSensitive: "包含 {name} 敏感檔案的貼文" userSaysSomethingSensitive: "包含 {name} 敏感檔案的貼文"
enableHorizontalSwipe: "滑動切換時間軸" enableHorizontalSwipe: "滑動切換時間軸"
loading: "載入中"
surrender: "退出"
gameRetry: "再試一次"
_bubbleGame: _bubbleGame:
howToPlay: "玩法說明" howToPlay: "玩法說明"
hold: "保留"
_score:
score: "分數"
scoreYen: "賺取的金額"
highScore: "最高分"
maxChain: "最大結合數"
yen: "{yen} 日圓"
estimatedQty: "{qty}個"
scoreSweets: "飯糰 {onigiriQtyWithUnit}"
_howToPlay: _howToPlay:
section1: "調整位置並將物體放入盒子中。" section1: "調整位置並將物體放入盒子中。"
section2: "當相同類型的物體黏在一起時,它們會變成不同的物體,您就會得到分數。" section2: "當相同類型的物體黏在一起時,它們會變成不同的物體,您就會得到分數。"
@ -1632,6 +1655,7 @@ _role:
gtlAvailable: "瀏覽全域時間軸" gtlAvailable: "瀏覽全域時間軸"
ltlAvailable: "瀏覽本地時間軸" ltlAvailable: "瀏覽本地時間軸"
canPublicNote: "允許公開貼文" canPublicNote: "允許公開貼文"
mentionMax: "貼文內的最大提及數"
canInvite: "發行伺服器邀請碼" canInvite: "發行伺服器邀請碼"
inviteLimit: "可建立邀請碼的數量" inviteLimit: "可建立邀請碼的數量"
inviteLimitCycle: "邀請碼的發放間隔" inviteLimitCycle: "邀請碼的發放間隔"
@ -1655,6 +1679,7 @@ _role:
canUseTranslator: "使用翻譯功能" canUseTranslator: "使用翻譯功能"
avatarDecorationLimit: "頭像裝飾的最大設置量" avatarDecorationLimit: "頭像裝飾的最大設置量"
_condition: _condition:
roleAssignedTo: "手動指派角色完成"
isLocal: "本地使用者" isLocal: "本地使用者"
isRemote: "遠端使用者" isRemote: "遠端使用者"
createdLessThan: "帳戶加入時間不超過" createdLessThan: "帳戶加入時間不超過"
@ -1755,6 +1780,8 @@ _aboutMisskey:
contributors: "主要貢獻者" contributors: "主要貢獻者"
allContributors: "全體貢獻人員" allContributors: "全體貢獻人員"
source: "原始碼" source: "原始碼"
original: "原始"
thisIsModifiedVersion: "{name} 使用原始 Misskey 的修改版本。"
translation: "翻譯 Misskey" translation: "翻譯 Misskey"
donate: "贊助 Misskey" donate: "贊助 Misskey"
morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰" morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
@ -2096,7 +2123,7 @@ _poll:
deadlineTime: "小時" deadlineTime: "小時"
duration: "時長" duration: "時長"
votesCount: "{n} 票" votesCount: "{n} 票"
totalVotes: "一共{n}票" totalVotes: "合計 {n} 票"
vote: "投票" vote: "投票"
showResult: "顯示結果" showResult: "顯示結果"
voted: "已投票" voted: "已投票"
@ -2273,6 +2300,7 @@ _notification:
reactedBySomeUsers: "{n}人做出了反應" reactedBySomeUsers: "{n}人做出了反應"
renotedBySomeUsers: "{n}人做了轉發" renotedBySomeUsers: "{n}人做了轉發"
followedBySomeUsers: "被{n}人追隨了" followedBySomeUsers: "被{n}人追隨了"
flushNotification: "重置通知歷史紀錄"
_types: _types:
all: "全部 " all: "全部 "
note: "使用者的最新貼文" note: "使用者的最新貼文"
@ -2358,7 +2386,7 @@ _moderationLogTypes:
updateCustomEmoji: "更新自訂表情符號" updateCustomEmoji: "更新自訂表情符號"
deleteCustomEmoji: "刪除自訂表情符號" deleteCustomEmoji: "刪除自訂表情符號"
updateServerSettings: "更新伺服器設定" updateServerSettings: "更新伺服器設定"
updateUserNote: "更新管理筆記" updateUserNote: "更新了使用者的管理筆記"
deleteDriveFile: "刪除檔案" deleteDriveFile: "刪除檔案"
deleteNote: "刪除貼文" deleteNote: "刪除貼文"
createGlobalAnnouncement: "建立全網通知" createGlobalAnnouncement: "建立全網通知"
@ -2370,6 +2398,7 @@ _moderationLogTypes:
resetPassword: "重設密碼" resetPassword: "重設密碼"
suspendRemoteInstance: "封鎖遠端伺服器" suspendRemoteInstance: "封鎖遠端伺服器"
unsuspendRemoteInstance: "解除封鎖遠端伺服器" unsuspendRemoteInstance: "解除封鎖遠端伺服器"
updateRemoteInstanceNote: "更新了遠端伺服器的管理筆記"
markSensitiveDriveFile: "標記為敏感檔案" markSensitiveDriveFile: "標記為敏感檔案"
unmarkSensitiveDriveFile: "撤銷標記為敏感檔案" unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
resolveAbuseReport: "解決檢舉" resolveAbuseReport: "解決檢舉"
@ -2490,6 +2519,8 @@ _reversi:
opponentHasSettingsChanged: "對手更改了設定" opponentHasSettingsChanged: "對手更改了設定"
allowIrregularRules: "允許異常規則(完全自由)" allowIrregularRules: "允許異常規則(完全自由)"
disallowIrregularRules: "不允許異常規則" disallowIrregularRules: "不允許異常規則"
showBoardLabels: "在棋盤上顯示行、列號"
useAvatarAsStone: "用大頭貼當作棋子"
_offlineScreen: _offlineScreen:
title: "離線-無法連接伺服器" title: "離線-無法連接伺服器"
header: "無法連接伺服器" header: "無法連接伺服器"

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.2.0-beta.11", "version": "2024.3.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@8.15.1", "packageManager": "pnpm@8.15.4",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",
@ -48,22 +48,22 @@
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"dependencies": { "dependencies": {
"cssnano": "6.0.3", "cssnano": "6.0.5",
"execa": "8.0.1", "execa": "8.0.1",
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"ignore-walk": "6.0.4", "ignore-walk": "6.0.4",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.4.33", "postcss": "8.4.35",
"tar": "6.2.0", "tar": "6.2.0",
"terser": "5.27.0", "terser": "5.28.1",
"typescript": "5.3.3" "typescript": "5.3.3"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "6.18.1", "@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "6.18.1", "@typescript-eslint/parser": "7.1.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.6.3", "cypress": "13.6.6",
"eslint": "8.56.0", "eslint": "8.57.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"start-server-and-test": "2.0.3" "start-server-and-test": "2.0.3"
}, },

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class MakeRepositoryUrlNullable1707808106310 {
name = 'MakeRepositoryUrlNullable1707808106310'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" DROP NOT NULL`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET NOT NULL`);
}
}

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RepositoryUrlFromSyuiloToMisskeyDev1708266695091 {
name = 'RepositoryUrlFromSyuiloToMisskeyDev1708266695091'
async up(queryRunner) {
await queryRunner.query(`UPDATE "meta" SET "repositoryUrl" = 'https://github.com/misskey-dev/misskey' WHERE "repositoryUrl" = 'https://github.com/syuilo/misskey'`);
}
async down(queryRunner) {
// no valid down migration
}
}

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class PerInstanceModNote1708399372194 {
name = 'PerInstanceModNote1708399372194'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "moderationNote" character varying(16384) NOT NULL DEFAULT ''`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "moderationNote"`);
}
}

View File

@ -67,9 +67,9 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.412.0", "@aws-sdk/client-s3": "3.412.0",
"@aws-sdk/lib-storage": "3.412.0", "@aws-sdk/lib-storage": "3.412.0",
"@bull-board/api": "5.14.0", "@bull-board/api": "5.14.2",
"@bull-board/fastify": "5.14.0", "@bull-board/fastify": "5.14.2",
"@bull-board/ui": "5.14.0", "@bull-board/ui": "5.14.2",
"@discordapp/twemoji": "15.0.2", "@discordapp/twemoji": "15.0.2",
"@fastify/accepts": "4.3.0", "@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1", "@fastify/cookie": "9.3.1",
@ -79,13 +79,13 @@
"@fastify/multipart": "8.1.0", "@fastify/multipart": "8.1.0",
"@fastify/static": "6.12.0", "@fastify/static": "6.12.0",
"@fastify/view": "8.2.0", "@fastify/view": "8.2.0",
"@misskey-dev/sharp-read-bmp": "^1.1.1", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "^5.0.3", "@misskey-dev/summaly": "5.0.3",
"@nestjs/common": "10.2.10", "@nestjs/common": "10.3.3",
"@nestjs/core": "10.2.10", "@nestjs/core": "10.3.3",
"@nestjs/testing": "10.2.10", "@nestjs/testing": "10.3.3",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "9.0.2", "@simplewebauthn/server": "9.0.3",
"@sinonjs/fake-timers": "11.2.2", "@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.1.10", "@smithy/node-http-handler": "2.1.10",
"@swc/cli": "0.1.63", "@swc/cli": "0.1.63",
@ -98,7 +98,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "5.1.9", "bullmq": "5.4.0",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -115,10 +115,11 @@
"file-type": "19.0.0", "file-type": "19.0.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "14.1.0", "got": "14.2.0",
"happy-dom": "10.0.3", "happy-dom": "10.0.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"http-link-header": "1.1.1", "htmlescape": "1.1.1",
"http-link-header": "1.1.2",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
"ipaddr.js": "2.1.0", "ipaddr.js": "2.1.0",
@ -127,7 +128,7 @@
"jsdom": "23.2.0", "jsdom": "23.2.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.2", "jsonld": "8.3.2",
"jsrsasign": "11.0.0", "jsrsasign": "11.1.0",
"meilisearch": "0.37.0", "meilisearch": "0.37.0",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"microformats-parser": "2.0.2", "microformats-parser": "2.0.2",
@ -135,10 +136,10 @@
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nanoid": "5.0.4", "nanoid": "5.0.6",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.8", "nodemailer": "6.9.10",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
@ -158,19 +159,19 @@
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.20.9", "re2": "1.20.9",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.1.14", "reflect-metadata": "0.2.1",
"rename": "1.0.4", "rename": "1.0.4",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"sanitize-html": "2.11.0", "sanitize-html": "2.12.1",
"secure-json-parse": "2.7.0", "secure-json-parse": "2.7.0",
"sharp": "0.32.6", "sharp": "0.33.2",
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"systeminformation": "5.21.24", "systeminformation": "5.22.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.2",
"tsc-alias": "1.8.8", "tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typeorm": "0.3.20", "typeorm": "0.3.20",
@ -184,17 +185,17 @@
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@nestjs/platform-express": "10.3.1", "@nestjs/platform-express": "10.3.3",
"@simplewebauthn/typescript-types": "8.3.4", "@simplewebauthn/types": "9.0.1",
"@swc/jest": "0.2.31", "@swc/jest": "0.2.31",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
"@types/archiver": "6.0.2", "@types/archiver": "6.0.2",
"@types/bcryptjs": "2.4.6", "@types/bcryptjs": "2.4.6",
"@types/body-parser": "1.19.5", "@types/body-parser": "1.19.5",
"@types/cbor": "6.0.0",
"@types/color-convert": "2.0.3", "@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8", "@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24", "@types/fluent-ffmpeg": "2.1.24",
"@types/htmlescape": "^1.1.3",
"@types/http-link-header": "1.0.5", "@types/http-link-header": "1.0.5",
"@types/jest": "29.5.11", "@types/jest": "29.5.11",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
@ -203,22 +204,21 @@
"@types/jsrsasign": "10.5.12", "@types/jsrsasign": "10.5.12",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "20.11.17", "@types/node": "20.11.22",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.14", "@types/nodemailer": "6.4.14",
"@types/oauth": "0.9.4", "@types/oauth": "0.9.4",
"@types/oauth2orize": "1.11.3", "@types/oauth2orize": "1.11.3",
"@types/oauth2orize-pkce": "0.1.2", "@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.0", "@types/pg": "8.11.2",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/punycode": "2.1.3", "@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/random-seed": "0.3.5", "@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6", "@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7", "@types/rename": "1.0.7",
"@types/sanitize-html": "2.9.5", "@types/sanitize-html": "2.11.0",
"@types/semver": "7.5.6", "@types/semver": "7.5.8",
"@types/sharp": "0.32.0",
"@types/simple-oauth2": "5.0.7", "@types/simple-oauth2": "5.0.7",
"@types/sinonjs__fake-timers": "8.1.5", "@types/sinonjs__fake-timers": "8.1.5",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
@ -226,17 +226,17 @@
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.3", "@types/web-push": "3.6.3",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "6.18.1", "@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "6.18.1", "@typescript-eslint/parser": "7.1.0",
"aws-sdk-client-mock": "3.0.1", "aws-sdk-client-mock": "3.0.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.56.0", "eslint": "8.57.0",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"execa": "8.0.1", "execa": "8.0.1",
"fkill": "^9.0.0", "fkill": "^9.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-mock": "29.7.0", "jest-mock": "29.7.0",
"nodemon": "3.0.3", "nodemon": "3.1.0",
"pid-port": "1.0.0", "pid-port": "1.0.0",
"simple-oauth2": "5.0.0" "simple-oauth2": "5.0.0"
} }

View File

@ -57,6 +57,8 @@ type Source = {
scope?: 'local' | 'global' | string[]; scope?: 'local' | 'global' | string[];
}; };
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
proxy?: string; proxy?: string;
proxySmtp?: string; proxySmtp?: string;
proxyBypassHosts?: string[]; proxyBypassHosts?: string[];
@ -145,6 +147,7 @@ export type Config = {
signToActivityPubGet: boolean | undefined; signToActivityPubGet: boolean | undefined;
version: string; version: string;
publishTarballInsteadOfProvideRepositoryUrl: boolean;
host: string; host: string;
hostname: string; hostname: string;
scheme: string; scheme: string;
@ -209,6 +212,7 @@ export function loadConfig(): Config {
return { return {
version, version,
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
url: url.origin, url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '', 10), port: config.port ?? parseInt(process.env.PORT ?? '', 10),
socket: config.socket, socket: config.socket,

View File

@ -20,7 +20,6 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { CacheService } from '@/core/CacheService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
@ -60,7 +59,6 @@ export class AccountMoveService {
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
private metaService: MetaService, private metaService: MetaService,
private relayService: RelayService, private relayService: RelayService,
private cacheService: CacheService,
private queueService: QueueService, private queueService: QueueService,
) { ) {
} }
@ -84,7 +82,7 @@ export class AccountMoveService {
Object.assign(src, update); Object.assign(src, update);
// Update cache // Update cache
this.cacheService.uriPersonCache.set(srcUri, src); this.globalEventService.publishInternalEvent('localUserUpdated', src);
const srcPerson = await this.apRendererService.renderPerson(src); const srcPerson = await this.apRendererService.renderPerson(src);
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src)); const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));

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
@ -159,11 +128,22 @@ export class CacheService implements OnApplicationShutdown {
const { type, body } = obj.message as GlobalEvents['internal']['payload']; const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) { switch (type) {
case 'userChangeSuspendedState': case 'userChangeSuspendedState':
case 'remoteUserUpdated': { case 'userChangeDeletedState':
const user = await this.usersRepository.findOneByOrFail({ id: body.id }); case 'remoteUserUpdated':
case 'localUserUpdated': {
const user = await this.usersRepository.findOneBy({ id: body.id });
if (user == null) {
this.userByIdCache.delete(body.id);
this.localUserByIdCache.delete(body.id);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value?.id === body.id) {
this.uriPersonCache.delete(k);
}
}
} else {
this.userByIdCache.set(user.id, user); this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) { for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value === user.id) { if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user); this.uriPersonCache.set(k, user);
} }
} }
@ -171,6 +151,7 @@ export class CacheService implements OnApplicationShutdown {
this.localUserByNativeTokenCache.set(user.token!, user); this.localUserByNativeTokenCache.set(user.token!, user);
this.localUserByIdCache.set(user.id, user); this.localUserByIdCache.set(user.id, user);
} }
}
break; break;
} }
case 'userTokenRegenerated': { case 'userTokenRegenerated': {

View File

@ -116,6 +116,7 @@ import { FlashEntityService } from './entities/FlashEntityService.js';
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
import { RoleEntityService } from './entities/RoleEntityService.js'; import { RoleEntityService } from './entities/RoleEntityService.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
import { MetaEntityService } from './entities/MetaEntityService.js';
import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApAudienceService } from './activitypub/ApAudienceService.js';
import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
@ -254,6 +255,7 @@ const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisti
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService }; const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@ -393,6 +395,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FlashLikeEntityService, FlashLikeEntityService,
RoleEntityService, RoleEntityService,
ReversiGameEntityService, ReversiGameEntityService,
MetaEntityService,
ApAudienceService, ApAudienceService,
ApDbResolverService, ApDbResolverService,
@ -528,6 +531,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FlashLikeEntityService, $FlashLikeEntityService,
$RoleEntityService, $RoleEntityService,
$ReversiGameEntityService, $ReversiGameEntityService,
$MetaEntityService,
$ApAudienceService, $ApAudienceService,
$ApDbResolverService, $ApDbResolverService,
@ -663,6 +667,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FlashLikeEntityService, FlashLikeEntityService,
RoleEntityService, RoleEntityService,
ReversiGameEntityService, ReversiGameEntityService,
MetaEntityService,
ApAudienceService, ApAudienceService,
ApDbResolverService, ApDbResolverService,
@ -797,6 +802,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FlashLikeEntityService, $FlashLikeEntityService,
$RoleEntityService, $RoleEntityService,
$ReversiGameEntityService, $ReversiGameEntityService,
$MetaEntityService,
$ApAudienceService, $ApAudienceService,
$ApDbResolverService, $ApDbResolverService,

View File

@ -393,6 +393,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
return this.emojisRepository.findOneBy({ id }); return this.emojisRepository.findOneBy({ id });
} }
@bindThis
public getEmojiByName(name: string): Promise<MiEmoji | null> {
return this.emojisRepository.findOneBy({ name, host: IsNull() });
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.cache.dispose(); this.cache.dispose();

View File

@ -9,6 +9,7 @@ import { QueueService } from '@/core/QueueService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
@Injectable() @Injectable()
export class DeleteAccountService { export class DeleteAccountService {
@ -18,6 +19,7 @@ export class DeleteAccountService {
private userSuspendService: UserSuspendService, private userSuspendService: UserSuspendService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService,
) { ) {
} }
@ -39,5 +41,7 @@ export class DeleteAccountService {
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
isDeleted: true, isDeleted: true,
}); });
this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true });
} }
} }

View File

@ -15,6 +15,7 @@ import isSvg from 'is-svg';
import probeImageSize from 'probe-image-size'; import probeImageSize from 'probe-image-size';
import { type predictionType } from 'nsfwjs'; import { type predictionType } from 'nsfwjs';
import sharp from 'sharp'; import sharp from 'sharp';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import { encode } from 'blurhash'; import { encode } from 'blurhash';
import { createTempDir } from '@/misc/create-temp.js'; import { createTempDir } from '@/misc/create-temp.js';
import { AiService } from '@/core/AiService.js'; import { AiService } from '@/core/AiService.js';
@ -122,7 +123,7 @@ export class FileInfoService {
'image/avif', 'image/avif',
'image/svg+xml', 'image/svg+xml',
].includes(type.mime)) { ].includes(type.mime)) {
blurhash = await this.getBlurhash(path).catch(e => { blurhash = await this.getBlurhash(path, type.mime).catch(e => {
warnings.push(`getBlurhash failed: ${e}`); warnings.push(`getBlurhash failed: ${e}`);
return undefined; return undefined;
}); });
@ -407,9 +408,9 @@ export class FileInfoService {
* Calculate average color of image * Calculate average color of image
*/ */
@bindThis @bindThis
private getBlurhash(path: string): Promise<string> { private getBlurhash(path: string, type: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
sharp(path) (await sharpBmp(path, type))
.raw() .raw()
.ensureAlpha() .ensureAlpha()
.resize(64, 64, { fit: 'inside' }) .resize(64, 64, { fit: 'inside' })

View File

@ -69,6 +69,7 @@ export interface MainEventTypes {
file: Packed<'DriveFile'>; file: Packed<'DriveFile'>;
}; };
readAllNotifications: undefined; readAllNotifications: undefined;
notificationFlushed: undefined;
unreadNotification: Packed<'Notification'>; unreadNotification: Packed<'Notification'>;
unreadMention: MiNote['id']; unreadMention: MiNote['id'];
readAllUnreadMentions: undefined; readAllUnreadMentions: undefined;
@ -209,8 +210,10 @@ type SerializedAll<T> = {
export interface InternalEventTypes { export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; }; userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
remoteUserUpdated: { id: MiUser['id']; }; remoteUserUpdated: { id: MiUser['id']; };
localUserUpdated: { id: MiUser['id']; };
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };

View File

@ -14,9 +14,16 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import type { IObject } from '@/core/activitypub/type.js';
import type { Response } from 'node-fetch'; import type { Response } from 'node-fetch';
import type { URL } from 'node:url'; import type { URL } from 'node:url';
export type HttpRequestSendOptions = {
throwErrorWhenResponseNotOk: boolean;
validators?: ((res: Response) => void)[];
};
@Injectable() @Injectable()
export class HttpRequestService { export class HttpRequestService {
/** /**
@ -104,6 +111,23 @@ export class HttpRequestService {
} }
} }
@bindThis
public async getActivityJson(url: string): Promise<IObject> {
const res = await this.send(url, {
method: 'GET',
headers: {
Accept: 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
},
timeout: 5000,
size: 1024 * 256,
}, {
throwErrorWhenResponseNotOk: true,
validators: [validateContentTypeSetAsActivityPub],
});
return await res.json() as IObject;
}
@bindThis @bindThis
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> { public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
const res = await this.send(url, { const res = await this.send(url, {
@ -132,17 +156,20 @@ export class HttpRequestService {
} }
@bindThis @bindThis
public async send(url: string, args: { public async send(
url: string,
args: {
method?: string, method?: string,
body?: string, body?: string,
headers?: Record<string, string>, headers?: Record<string, string>,
timeout?: number, timeout?: number,
size?: number, size?: number,
} = {}, extra: { } = {},
throwErrorWhenResponseNotOk: boolean; extra: HttpRequestSendOptions = {
} = {
throwErrorWhenResponseNotOk: true, throwErrorWhenResponseNotOk: true,
}): Promise<Response> { validators: [],
},
): Promise<Response> {
const timeout = args.timeout ?? 5000; const timeout = args.timeout ?? 5000;
const controller = new AbortController(); const controller = new AbortController();
@ -166,6 +193,12 @@ export class HttpRequestService {
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
} }
if (res.ok) {
for (const validator of (extra.validators ?? [])) {
validator(res);
}
}
return res; return res;
} }
} }

View File

@ -59,6 +59,8 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js'; import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackPromise } from '@/misc/promise-tracker.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -151,8 +153,6 @@ type Option = {
export class NoteCreateService implements OnApplicationShutdown { export class NoteCreateService implements OnApplicationShutdown {
#shutdownController = new AbortController(); #shutdownController = new AbortController();
public static ContainsProhibitedWordsError = class extends Error {};
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@ -263,8 +263,14 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) { const hasProhibitedWords = await this.checkProhibitedWordsContain({
throw new NoteCreateService.ContainsProhibitedWordsError(); cw: data.cw,
text: data.text,
pollChoices: data.poll?.choices,
}, meta.prohibitedWords);
if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
} }
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host); const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
@ -379,6 +385,10 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) {
throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions');
}
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
setImmediate('post created', { signal: this.#shutdownController.signal }).then( setImmediate('post created', { signal: this.#shutdownController.signal }).then(
@ -817,7 +827,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const mentions = extractMentions(tokens); const mentions = extractMentions(tokens);
let mentionedUsers = (await Promise.all(mentions.map(m => let mentionedUsers = (await Promise.all(mentions.map(m =>
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
))).filter(x => x != null) as MiUser[]; ))).filter(isNotNull);
// Drop duplicate users // Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) => mentionedUsers = mentionedUsers.filter((u, i, self) =>
@ -991,6 +1001,23 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
if (prohibitedWords == null) {
prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
}
if (
this.utilityService.isKeyWordIncluded(
this.utilityService.concatNoteContentsForKeyWordCheck(content),
prohibitedWords,
)
) {
return true;
}
return false;
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.#shutdownController.abort(); this.#shutdownController.abort();

View File

@ -88,22 +88,24 @@ export class NoteReadService implements OnApplicationShutdown {
userId: MiUser['id'], userId: MiUser['id'],
notes: (MiNote | Packed<'Note'>)[], notes: (MiNote | Packed<'Note'>)[],
): Promise<void> { ): Promise<void> {
const readMentions: (MiNote | Packed<'Note'>)[] = []; if (notes.length === 0) return;
const readSpecifiedNotes: (MiNote | Packed<'Note'>)[] = [];
const noteIds = new Set<MiNote['id']>();
for (const note of notes) { for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) { if (note.mentions && note.mentions.includes(userId)) {
readMentions.push(note); noteIds.add(note.id);
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note); noteIds.add(note.id);
} }
} }
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) { if (noteIds.size === 0) return;
// Remove the record // Remove the record
await this.noteUnreadsRepository.delete({ await this.noteUnreadsRepository.delete({
userId: userId, userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), noteId: In(Array.from(noteIds)),
}); });
// TODO: ↓まとめてクエリしたい // TODO: ↓まとめてクエリしたい
@ -128,7 +130,6 @@ export class NoteReadService implements OnApplicationShutdown {
} }
})); }));
} }
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {

View File

@ -122,6 +122,14 @@ export class NotificationService implements OnApplicationShutdown {
return null; return null;
} }
} else if (recieveConfig?.type === 'mutualFollow') { } else if (recieveConfig?.type === 'mutualFollow') {
const [isFollowing, isFollower] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
]);
if (!(isFollowing && isFollower)) {
return null;
}
} else if (recieveConfig?.type === 'followingOrFollower') {
const [isFollowing, isFollower] = await Promise.all([ const [isFollowing, isFollower] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
@ -155,6 +163,8 @@ export class NotificationService implements OnApplicationShutdown {
const packed = await this.notificationEntityService.pack(notification, notifieeId, {}); const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
if (packed == null) return null;
// Publish notification event // Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed); this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
@ -204,6 +214,15 @@ export class NotificationService implements OnApplicationShutdown {
*/ */
} }
@bindThis
public async flushAllNotifications(userId: MiUser['id']) {
await Promise.all([
this.redisClient.del(`notificationTimeline:${userId}`),
this.redisClient.del(`latestReadNotification:${userId}`),
]);
this.globalEventService.publishMainStream(userId, 'notificationFlushed');
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.#shutdownController.abort(); this.#shutdownController.abort();

View File

@ -115,12 +115,19 @@ export class PushNotificationService implements OnApplicationShutdown {
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
auth: subscription.auth, auth: subscription.auth,
publickey: subscription.publickey, publickey: subscription.publickey,
}).then(() => {
this.refreshCache(userId);
}); });
} }
}); });
} }
} }
@bindThis
public refreshCache(userId: string): void {
this.subscriptionsCache.refresh(userId);
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.subscriptionsCache.dispose(); this.subscriptionsCache.dispose();

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) {
@ -322,35 +322,36 @@ export class ReactionService {
//#endregion //#endregion
} }
/**
*
* 0
*/
@bindThis @bindThis
public convertLegacyReactions(reactions: Record<string, number>) { public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
const _reactions = {} as Record<string, number>; return Object.entries(reactions)
.filter(([, count]) => {
// `ReactionService.prototype.delete`ではリアクション削除時に、
// `MiNote['reactions']`のエントリの値をデクリメントしているが、
// デクリメントしているだけなのでエントリ自体は0を値として持つ形で残り続ける。
// そのため、この処理がなければ、「0個のリアクションがついている」ということになってしまう。
return count > 0;
})
.map(([reaction, count]) => {
// unchecked indexed access
const convertedReaction = legacies[reaction] as string | undefined;
for (const reaction of Object.keys(reactions)) { const key = this.decodeReaction(convertedReaction ?? reaction).reaction;
if (reactions[reaction] <= 0) continue;
if (Object.keys(legacies).includes(reaction)) { return [key, count] as const;
if (_reactions[legacies[reaction]]) { })
_reactions[legacies[reaction]] += reactions[reaction]; .reduce<MiNote['reactions']>((acc, [key, count]) => {
} else { // unchecked indexed access
_reactions[legacies[reaction]] = reactions[reaction]; const prevCount = acc[key] as number | undefined;
}
} else {
if (_reactions[reaction]) {
_reactions[reaction] += reactions[reaction];
} else {
_reactions[reaction] = reactions[reaction];
}
}
}
const _reactions2 = {} as Record<string, number>; acc[key] = (prevCount ?? 0) + count;
for (const reaction of Object.keys(_reactions)) { return acc;
_reactions2[this.decodeReaction(reaction).reaction] = _reactions[reaction]; }, {});
}
return _reactions2;
} }
@bindThis @bindThis

View File

@ -35,6 +35,7 @@ export type RolePolicies = {
gtlAvailable: boolean; gtlAvailable: boolean;
ltlAvailable: boolean; ltlAvailable: boolean;
canPublicNote: boolean; canPublicNote: boolean;
mentionLimit: number;
canInvite: boolean; canInvite: boolean;
inviteLimit: number; inviteLimit: number;
inviteLimitCycle: number; inviteLimitCycle: number;
@ -62,6 +63,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true, gtlAvailable: true,
ltlAvailable: true, ltlAvailable: true,
canPublicNote: true, canPublicNote: true,
mentionLimit: 20,
canInvite: false, canInvite: false,
inviteLimit: 0, inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7, inviteLimitCycle: 60 * 24 * 7,
@ -200,17 +202,20 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
} }
@bindThis @bindThis
private evalCond(user: MiUser, value: RoleCondFormulaValue): boolean { private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
try { try {
switch (value.type) { switch (value.type) {
case 'and': { case 'and': {
return value.values.every(v => this.evalCond(user, v)); return value.values.every(v => this.evalCond(user, roles, v));
} }
case 'or': { case 'or': {
return value.values.some(v => this.evalCond(user, v)); return value.values.some(v => this.evalCond(user, roles, v));
} }
case 'not': { case 'not': {
return !this.evalCond(user, value.value); return !this.evalCond(user, roles, value.value);
}
case 'roleAssignedTo': {
return roles.some(r => r.id === value.roleId);
} }
case 'isLocal': { case 'isLocal': {
return this.userEntityService.isLocalUser(user); return this.userEntityService.isLocalUser(user);
@ -272,7 +277,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
const assigns = await this.getUserAssigns(userId); const assigns = await this.getUserAssigns(userId);
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id)); const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula));
return [...assignedRoles, ...matchedCondRoles]; return [...assignedRoles, ...matchedCondRoles];
} }
@ -285,13 +290,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
// 期限切れのロールを除外 // 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
const assignedBadgeRoles = assignedRoles.filter(r => r.asBadge);
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional')); const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
if (badgeCondRoles.length > 0) { if (badgeCondRoles.length > 0) {
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula)); const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula));
return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
} else { } else {
return assignedBadgeRoles; return assignedBadgeRoles;
@ -325,6 +330,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),

View File

@ -30,6 +30,7 @@ import type { Config } from '@/config.js';
import { AccountMoveService } from '@/core/AccountMoveService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { ThinUser } from '@/queue/types.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -94,21 +95,35 @@ export class UserFollowingService implements OnModuleInit {
this.userBlockingService = this.moduleRef.get('UserBlockingService'); this.userBlockingService = this.moduleRef.get('UserBlockingService');
} }
@bindThis
public async deliverAccept(follower: MiRemoteUser, followee: MiPartialLocalUser, requestId?: string) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
@bindThis @bindThis
public async follow( public async follow(
_follower: { id: MiUser['id'] }, _follower: ThinUser,
_followee: { id: MiUser['id'] }, _followee: ThinUser,
{ requestId, silent = false, withReplies }: { { requestId, silent = false, withReplies }: {
requestId?: string, requestId?: string,
silent?: boolean, silent?: boolean,
withReplies?: boolean, withReplies?: boolean,
} = {}, } = {},
): Promise<void> { ): Promise<void> {
/**
*
*/
const [follower, followee] = await Promise.all([ const [follower, followee] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: _follower.id }), this.usersRepository.findOneByOrFail({ id: _follower.id }),
this.usersRepository.findOneByOrFail({ id: _followee.id }), this.usersRepository.findOneByOrFail({ id: _followee.id }),
]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser];
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isRemoteUser(followee)) {
// What?
throw new Error('Remote user cannot follow remote user.');
}
// check blocking // check blocking
const [blocking, blocked] = await Promise.all([ const [blocking, blocked] = await Promise.all([
this.userBlockingService.checkBlocked(follower.id, followee.id), this.userBlockingService.checkBlocked(follower.id, followee.id),
@ -129,6 +144,24 @@ export class UserFollowingService implements OnModuleInit {
if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
} }
if (await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: followee.id,
},
})) {
// すでにフォロー関係が存在している場合
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
// リモート → ローカル: acceptを送り返しておしまい
this.deliverAccept(follower, followee, requestId);
return;
}
if (this.userEntityService.isLocalUser(follower)) {
// ローカル → リモート/ローカル: 例外
throw new IdentifiableError('ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced', 'already following');
}
}
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id }); const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
// フォロー対象が鍵アカウントである or // フォロー対象が鍵アカウントである or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
@ -189,8 +222,7 @@ export class UserFollowingService implements OnModuleInit {
await this.insertFollowingDoc(followee, follower, silent, withReplies); await this.insertFollowingDoc(followee, follower, silent, withReplies);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); this.deliverAccept(follower, followee, requestId);
this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@ -571,8 +603,7 @@ export class UserFollowingService implements OnModuleInit {
await this.insertFollowingDoc(followee, follower, false, request.withReplies); await this.insertFollowingDoc(followee, follower, false, request.withReplies);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee)); this.deliverAccept(follower, followee as MiPartialLocalUser, request.requestId ?? undefined);
this.queueService.deliver(followee, content, follower.inbox, false);
} }
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {

View File

@ -42,6 +42,20 @@ export class UtilityService {
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
} }
@bindThis
public concatNoteContentsForKeyWordCheck(content: {
cw?: string | null;
text?: string | null;
pollChoices?: string[] | null;
others?: string[] | null;
}): string {
/**
*
* cwとtextは内容が繋がっているかもしれないので間に何も入れずにチェックする
*/
return `${content.cw ?? ''}${content.text ?? ''}\n${(content.pollChoices ?? []).join('\n')}\n${(content.others ?? []).join('\n')}`;
}
@bindThis @bindThis
public isKeyWordIncluded(text: string, keyWords: string[]): boolean { public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
if (keyWords.length === 0) return false; if (keyWords.length === 0) return false;

View File

@ -26,7 +26,7 @@ import type {
PublicKeyCredentialDescriptorFuture, PublicKeyCredentialDescriptorFuture,
PublicKeyCredentialRequestOptionsJSON, PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON, RegistrationResponseJSON,
} from '@simplewebauthn/typescript-types'; } from '@simplewebauthn/types';
@Injectable() @Injectable()
export class WebAuthnService { export class WebAuthnService {
@ -191,7 +191,7 @@ export class WebAuthnService {
if (cert[0] === 0x04) { // 前の実装ではいつも 0x04 で始まっていた if (cert[0] === 0x04) { // 前の実装ではいつも 0x04 で始まっていた
const halfLength = (cert.length - 1) / 2; const halfLength = (cert.length - 1) / 2;
const cborMap = new Map<number, number | ArrayBufferLike>(); const cborMap = new Map<number, number | Uint8Array>();
cborMap.set(1, 2); // kty, EC2 cborMap.set(1, 2); // kty, EC2
cborMap.set(3, -7); // alg, ES256 cborMap.set(3, -7); // alg, ES256
cborMap.set(-1, 1); // crv, P256 cborMap.set(-1, 1); // crv, P256

View File

@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit';
import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { concat, unique } from '@/misc/prelude/array.js'; import { concat, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApIds } from './type.js'; import { getApIds } from './type.js';
import { ApPersonService } from './models/ApPersonService.js'; import { ApPersonService } from './models/ApPersonService.js';
import type { ApObject } from './type.js'; import type { ApObject } from './type.js';
@ -40,7 +41,7 @@ export class ApAudienceService {
const limit = promiseLimit<MiUser | null>(2); const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
)).filter((x): x is MiUser => x != null); )).filter(isNotNull);
if (toGroups.public.length > 0) { if (toGroups.public.length > 0) {
return { return {

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

@ -27,6 +27,7 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js'; import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js'; import { ApLoggerService } from './ApLoggerService.js';
@ -35,6 +36,7 @@ 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 { 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';
@ -82,6 +84,7 @@ export class ApInboxService {
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
private apQuestionService: ApQuestionService, private apQuestionService: ApQuestionService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService,
) { ) {
this.logger = this.apLoggerService.logger; this.logger = this.apLoggerService.logger;
} }
@ -479,6 +482,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}`;
} }
@ -515,7 +520,7 @@ export class ApInboxService {
const userIds = uris const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/')) .filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1)) .map(uri => uri.split('/').at(-1))
.filter((userId): userId is string => userId !== undefined); .filter(isNotNull);
const users = await this.usersRepository.findBy({ const users = await this.usersRepository.findBy({
id: In(userIds), id: In(userIds),
}); });

View File

@ -315,7 +315,7 @@ export class ApRendererService {
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => { const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
if (ids.length === 0) return []; if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) }); const items = await this.driveFilesRepository.findBy({ id: In(ids) });
return ids.map(id => items.find(item => item.id === id)).filter((item): item is MiDriveFile => item != null); return ids.map(id => items.find(item => item.id === id)).filter(isNotNull);
}; };
let inReplyTo; let inReplyTo;

View File

@ -14,6 +14,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
type Request = { type Request = {
url: string; url: string;
@ -70,7 +71,7 @@ export class ApRequestCreator {
url: u.href, url: u.href,
method: 'GET', method: 'GET',
headers: this.#objectAssignWithLcKey({ headers: this.#objectAssignWithLcKey({
'Accept': 'application/activity+json, application/ld+json', 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'Date': new Date().toUTCString(), 'Date': new Date().toUTCString(),
'Host': new URL(args.url).host, 'Host': new URL(args.url).host,
}, args.additionalHeaders), }, args.additionalHeaders),
@ -195,6 +196,9 @@ export class ApRequestService {
const res = await this.httpRequestService.send(url, { const res = await this.httpRequestService.send(url, {
method: req.request.method, method: req.request.method,
headers: req.request.headers, headers: req.request.headers,
}, {
throwErrorWhenResponseNotOk: true,
validators: [validateContentTypeSetAsActivityPub],
}); });
return await res.json(); return await res.json();

View File

@ -105,7 +105,7 @@ export class Resolver {
const object = (this.user const object = (this.user
? await this.apRequestService.signedGet(value, this.user) as IObject ? await this.apRequestService.signedGet(value, this.user) as IObject
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; : await this.httpRequestService.getActivityJson(value)) as IObject;
if ( if (
Array.isArray(object['@context']) ? Array.isArray(object['@context']) ?

View File

@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CONTEXTS } from './misc/contexts.js'; import { CONTEXTS } from './misc/contexts.js';
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
import type { JsonLdDocument } from 'jsonld'; import type { JsonLdDocument } from 'jsonld';
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js'; import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
@ -133,7 +134,10 @@ class LdSignature {
}, },
timeout: this.loderTimeout, timeout: this.loderTimeout,
}, },
{ throwErrorWhenResponseNotOk: false }, {
throwErrorWhenResponseNotOk: false,
validators: [validateContentTypeSetAsJsonLD],
},
).then(res => { ).then(res => {
if (!res.ok) { if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`); throw new Error(`${res.status} ${res.statusText}`);

View File

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Response } from 'node-fetch';
export function validateContentTypeSetAsActivityPub(response: Response): void {
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
if (contentType === '') {
throw new Error('Validate content type of AP response: No content-type header');
}
if (
contentType.startsWith('application/activity+json') ||
(contentType.startsWith('application/ld+json;') && contentType.includes('https://www.w3.org/ns/activitystreams'))
) {
return;
}
throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json');
}
const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/;
export function validateContentTypeSetAsJsonLD(response: Response): void {
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
if (contentType === '') {
throw new Error('Validate content type of JSON LD: No content-type header');
}
if (
contentType.startsWith('application/ld+json') ||
contentType.startsWith('application/json') ||
plusJsonSuffixRegex.test(contentType)
) {
return;
}
throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json');
}

View File

@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit';
import type { MiUser } from '@/models/_.js'; import type { MiUser } from '@/models/_.js';
import { toArray, unique } from '@/misc/prelude/array.js'; import { toArray, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isMention } from '../type.js'; import { isMention } from '../type.js';
import { Resolver } from '../ApResolverService.js'; import { Resolver } from '../ApResolverService.js';
import { ApPersonService } from './ApPersonService.js'; import { ApPersonService } from './ApPersonService.js';
@ -27,7 +28,7 @@ export class ApMentionService {
const limit = promiseLimit<MiUser | null>(2); const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
)).filter((x): x is MiUser => x != null); )).filter(isNotNull);
return mentionedUsers; return mentionedUsers;
} }

View File

@ -24,6 +24,8 @@ import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js'; import { ApMfmService } from '../ApMfmService.js';
@ -151,11 +153,47 @@ export class ApNoteService {
throw new Error('invalid note.attributedTo: ' + note.attributedTo); throw new Error('invalid note.attributedTo: ' + note.attributedTo);
} }
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser; const uri = getOneApId(note.attributedTo);
// 投稿者が凍結されていたらスキップ // ローカルで投稿者を検索し、もし凍結されていたらスキップ
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
if (cachedActor && cachedActor.isSuspended) {
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
}
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = extractApHashtags(note.tag);
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
text = note.source.content;
} else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content;
} else if (typeof note.content === 'string') {
text = this.apMfmService.htmlToMfm(note.content, note.tag);
}
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
//#region Contents Check
// 添付ファイルとユーザーをこのサーバーで登録する前に内容をチェックする
/**
*
*/
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
}
//#endregion
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
// 解決した投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
throw new Error('actor has been suspended'); throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
} }
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
@ -170,9 +208,6 @@ export class ApNoteService {
} }
} }
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = extractApHashtags(note.tag);
// 添付ファイル // 添付ファイル
// TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない // TODO: attachmentは必ずしも配列ではない
@ -221,7 +256,7 @@ export class ApNoteService {
} }
}; };
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
const results = await Promise.all(uris.map(tryResolveNote)); const results = await Promise.all(uris.map(tryResolveNote));
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
@ -232,18 +267,6 @@ export class ApNoteService {
} }
} }
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
text = note.source.content;
} else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content;
} else if (typeof note.content === 'string') {
text = this.apMfmService.htmlToMfm(note.content, note.tag);
}
// vote // vote
if (reply && reply.hasPoll) { if (reply && reply.hasPoll) {
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
@ -273,8 +296,6 @@ export class ApNoteService {
const apEmojis = emojis.map(emoji => emoji.name); const apEmojis = emojis.map(emoji => emoji.name);
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
try { try {
return await this.noteCreateService.create(actor, { return await this.noteCreateService.create(actor, {
createdAt: note.published ? new Date(note.published) : null, createdAt: note.published ? new Date(note.published) : null,

View File

@ -38,6 +38,7 @@ import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -636,7 +637,7 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持 // とりあえずidを別の時間で生成して順番を維持
let td = 0; let td = 0;
for (const note of featuredNotes.filter((note): note is MiNote => note != null)) { for (const note of featuredNotes.filter(isNotNull)) {
td -= 1000; td -= 1000;
transactionalEntityManager.insert(MiUserNotePining, { transactionalEntityManager.insert(MiUserNotePining, {
id: this.idService.gen(Date.now() + td), id: this.idService.gen(Date.now() + td),

View File

@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
import type { IPoll } from '@/models/Poll.js'; import type { IPoll } from '@/models/Poll.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isQuestion } from '../type.js'; import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js'; import { ApResolverService } from '../ApResolverService.js';
@ -51,7 +52,7 @@ export class ApQuestionService {
const choices = question[multiple ? 'anyOf' : 'oneOf'] const choices = question[multiple ? 'anyOf' : 'oneOf']
?.map((x) => x.name) ?.map((x) => x.name)
.filter((x): x is string => typeof x === 'string') .filter(isNotNull)
?? []; ?? [];
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);

View File

@ -4,6 +4,7 @@
*/ */
import { toArray } from '@/misc/prelude/array.js'; import { toArray } from '@/misc/prelude/array.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isHashtag } from '../type.js'; import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js'; import type { IObject, IApHashtag } from '../type.js';
@ -15,7 +16,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
return hashtags.map(tag => { return hashtags.map(tag => {
const m = tag.name.match(/^#(.+)/); const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null; return m ? m[1] : null;
}).filter((x): x is string => x != null); }).filter(isNotNull);
} }
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {

View File

@ -259,7 +259,7 @@ export class DriveFileEntityService {
options?: PackOptions, options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> { ): Promise<Packed<'DriveFile'>[]> {
const items = await Promise.all(files.map(f => this.packNullable(f, options))); const items = await Promise.all(files.map(f => this.packNullable(f, options)));
return items.filter((x): x is Packed<'DriveFile'> => x != null); return items.filter(isNotNull);
} }
@bindThis @bindThis

View File

@ -8,12 +8,15 @@ import type { Packed } from '@/misc/json-schema.js';
import type { MiInstance } from '@/models/Instance.js'; import type { MiInstance } from '@/models/Instance.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UtilityService } from '../UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { RoleService } from '@/core/RoleService.js';
import { MiUser } from '@/models/User.js';
@Injectable() @Injectable()
export class InstanceEntityService { export class InstanceEntityService {
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
private roleService: RoleService,
private utilityService: UtilityService, private utilityService: UtilityService,
) { ) {
@ -22,8 +25,11 @@ export class InstanceEntityService {
@bindThis @bindThis
public async pack( public async pack(
instance: MiInstance, instance: MiInstance,
me?: { id: MiUser['id']; } | null | undefined,
): Promise<Packed<'FederationInstance'>> { ): Promise<Packed<'FederationInstance'>> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
return { return {
id: instance.id, id: instance.id,
firstRetrievedAt: instance.firstRetrievedAt.toISOString(), firstRetrievedAt: instance.firstRetrievedAt.toISOString(),
@ -48,6 +54,7 @@ export class InstanceEntityService {
themeColor: instance.themeColor, themeColor: instance.themeColor,
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
moderationNote: iAmModerator ? instance.moderationNote : null,
}; };
} }

View File

@ -0,0 +1,154 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5';
import type { Packed } from '@/misc/json-schema.js';
import type { MiMeta } from '@/models/Meta.js';
import type { AdsRepository } from '@/models/_.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
@Injectable()
export class MetaEntityService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
private userEntityService: UserEntityService,
private metaService: MetaService,
private instanceActorService: InstanceActorService,
) { }
@bindThis
public async pack(meta?: MiMeta): Promise<Packed<'MetaLite'>> {
let instance = meta;
if (!instance) {
instance = await this.metaService.fetch();
}
const ads = await this.adsRepository.createQueryBuilder('ads')
.where('ads.expiresAt > :now', { now: new Date() })
.andWhere('ads.startsAt <= :now', { now: new Date() })
.andWhere(new Brackets(qb => {
// 曜日のビットフラグを確認する
qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() })
.orWhere('ads.dayOfWeek = 0');
}))
.getMany();
const packed: Packed<'MetaLite'> = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
version: this.config.version,
providesTarball: this.config.publishTarballInsteadOfProvideRepositoryUrl,
name: instance.name,
shortName: instance.shortName,
uri: this.config.url,
description: instance.description,
langs: instance.langs,
tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha,
mcaptchaSiteKey: instance.mcaptchaSitekey,
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
bannerUrl: instance.bannerUrl,
infoImageUrl: instance.infoImageUrl,
serverErrorImageUrl: instance.serverErrorImageUrl,
notFoundImageUrl: instance.notFoundImageUrl,
iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
// クライアントの手間を減らすためあらかじめJSONに変換しておく
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
place: ad.place,
ratio: ad.ratio,
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
})),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
serverRules: instance.serverRules,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
};
return packed;
}
@bindThis
public async packDetailed(meta?: MiMeta): Promise<Packed<'MetaDetailed'>> {
let instance = meta;
if (!instance) {
instance = await this.metaService.fetch();
}
const packed = await this.pack(instance);
const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null;
const packDetailed: Packed<'MetaDetailed'> = {
...packed,
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: !await this.instanceActorService.realLocalUsersPresent(),
proxyAccountName: proxyAccount ? proxyAccount.username : null,
features: {
localTimeline: instance.policies.ltlAvailable,
globalTimeline: instance.policies.gtlAvailable,
registration: !instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
hcaptcha: instance.enableHcaptcha,
recaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage,
serviceWorker: instance.enableServiceWorker,
miauth: true,
},
};
return packDetailed;
}
}

View File

@ -69,4 +69,19 @@ export class NoteReactionEntityService implements OnModuleInit {
} : {}), } : {}),
}; };
} }
@bindThis
public async packMany(
reactions: MiNoteReaction[],
me?: { id: MiUser['id'] } | null | undefined,
options?: {
withNote: boolean;
},
): Promise<Packed<'NoteReaction'>[]> {
const opts = Object.assign({
withNote: false,
}, options);
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts)));
}
} }

View File

@ -14,14 +14,14 @@ import type { MiNote } from '@/models/Note.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js'; import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, notificationTypes } from '@/types.js'; import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
import { CacheService } from '@/core/CacheService.js';
import { RoleEntityService } from './RoleEntityService.js'; import { RoleEntityService } from './RoleEntityService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]);
const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded']);
@Injectable() @Injectable()
export class NotificationEntityService implements OnModuleInit { export class NotificationEntityService implements OnModuleInit {
@ -41,6 +41,8 @@ export class NotificationEntityService implements OnModuleInit {
@Inject(DI.followRequestsRepository) @Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository, private followRequestsRepository: FollowRequestsRepository,
private cacheService: CacheService,
//private userEntityService: UserEntityService, //private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService, //private noteEntityService: NoteEntityService,
) { ) {
@ -52,130 +54,48 @@ export class NotificationEntityService implements OnModuleInit {
this.roleEntityService = this.moduleRef.get('RoleEntityService'); this.roleEntityService = this.moduleRef.get('RoleEntityService');
} }
@bindThis /**
public async pack( *
src: MiNotification, */
async #packInternal <T extends MiNotification | MiGroupedNotification> (
src: T,
meId: MiUser['id'], meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
options: { options: {
checkValidNotifier?: boolean;
}, },
hint?: { hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>; packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>; packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
}, },
): Promise<Packed<'Notification'>> { ): Promise<Packed<'Notification'> | null> {
const notification = src; const notification = src;
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
if (options.checkValidNotifier !== false && !(await this.#isValidNotifier(notification, meId))) return null;
const needsNote = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification;
const noteIfNeed = needsNote ? (
hint?.packedNotes != null hint?.packedNotes != null
? hint.packedNotes.get(notification.noteId) ? hint.packedNotes.get(notification.noteId)
: this.noteEntityService.pack(notification.noteId, { id: meId }, { : this.noteEntityService.pack(notification.noteId, { id: meId }, {
detail: true, detail: true,
}) })
) : undefined; ) : undefined;
const userIfNeed = 'notifierId' in notification ? ( // if the note has been deleted, don't show this notification
hint?.packedUsers != null if (needsNote && !noteIfNeed) return null;
? hint.packedUsers.get(notification.notifierId)
: this.userEntityService.pack(notification.notifierId, { id: meId }) const needsUser = 'notifierId' in notification;
) : undefined; const userIfNeed = needsUser ? (
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
return await awaitAll({
id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(),
type: notification.type,
userId: 'notifierId' in notification ? notification.notifierId : undefined,
...(userIfNeed != null ? { user: userIfNeed } : {}),
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
...(notification.type === 'reaction' ? {
reaction: notification.reaction,
} : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader,
icon: notification.customIcon,
} : {}),
});
}
@bindThis
public async packMany(
notifications: MiNotification[],
meId: MiUser['id'],
) {
if (notifications.length === 0) return [];
let validNotifications = notifications;
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) },
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
}) : [];
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
detail: true,
});
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
validNotifications = validNotifications.filter(x => !('noteId' in x) || packedNotes.has(x.noteId));
const userIds = validNotifications.map(x => 'notifierId' in x ? x.notifierId : null).filter(isNotNull);
const users = userIds.length > 0 ? await this.usersRepository.find({
where: { id: In(userIds) },
}) : [];
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId });
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
// 既に解決されたフォローリクエストの通知を除外
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
if (followRequestNotifications.length > 0) {
const reqs = await this.followRequestsRepository.find({
where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) },
});
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
}
return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, {
packedNotes,
packedUsers,
})));
}
@bindThis
public async packGrouped(
src: MiGroupedNotification,
meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types
options: {
},
hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
},
): Promise<Packed<'Notification'>> {
const notification = src;
const noteIfNeed = NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
hint?.packedNotes != null
? hint.packedNotes.get(notification.noteId)
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
detail: true,
})
) : undefined;
const userIfNeed = 'notifierId' in notification ? (
hint?.packedUsers != null hint?.packedUsers != null
? hint.packedUsers.get(notification.notifierId) ? hint.packedUsers.get(notification.notifierId)
: this.userEntityService.pack(notification.notifierId, { id: meId }) : this.userEntityService.pack(notification.notifierId, { id: meId })
) : undefined; ) : undefined;
// if the user has been deleted, don't show this notification
if (needsUser && !userIfNeed) return null;
// #region Grouped notifications
if (notification.type === 'reaction:grouped') { if (notification.type === 'reaction:grouped') {
const reactions = await Promise.all(notification.reactions.map(async reaction => { const reactions = (await Promise.all(notification.reactions.map(async reaction => {
const user = hint?.packedUsers != null const user = hint?.packedUsers != null
? hint.packedUsers.get(reaction.userId)! ? hint.packedUsers.get(reaction.userId)!
: await this.userEntityService.pack(reaction.userId, { id: meId }); : await this.userEntityService.pack(reaction.userId, { id: meId });
@ -183,7 +103,12 @@ export class NotificationEntityService implements OnModuleInit {
user, user,
reaction: reaction.reaction, reaction: reaction.reaction,
}; };
})); }))).filter(r => isNotNull(r.user));
// if all users have been deleted, don't show this notification
if (reactions.length === 0) {
return null;
}
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(), createdAt: new Date(notification.createdAt).toISOString(),
@ -192,14 +117,19 @@ export class NotificationEntityService implements OnModuleInit {
reactions, reactions,
}); });
} else if (notification.type === 'renote:grouped') { } else if (notification.type === 'renote:grouped') {
const users = await Promise.all(notification.userIds.map(userId => { const users = (await Promise.all(notification.userIds.map(userId => {
const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null; const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null;
if (packedUser) { if (packedUser) {
return packedUser; return packedUser;
} }
return this.userEntityService.pack(userId, { id: meId }); return this.userEntityService.pack(userId, { id: meId });
})); }))).filter(isNotNull);
// if all users have been deleted, don't show this notification
if (users.length === 0) {
return null;
}
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(), createdAt: new Date(notification.createdAt).toISOString(),
@ -208,8 +138,14 @@ export class NotificationEntityService implements OnModuleInit {
users, users,
}); });
} }
// #endregion
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined; const needsRole = notification.type === 'roleAssigned';
const role = needsRole ? await this.roleEntityService.pack(notification.roleId) : undefined;
// if the role has been deleted, don't show this notification
if (needsRole && !role) {
return null;
}
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
@ -235,15 +171,16 @@ export class NotificationEntityService implements OnModuleInit {
}); });
} }
@bindThis async #packManyInternal <T extends MiNotification | MiGroupedNotification> (
public async packGroupedMany( notifications: T[],
notifications: MiGroupedNotification[],
meId: MiUser['id'], meId: MiUser['id'],
) { ): Promise<T[]> {
if (notifications.length === 0) return []; if (notifications.length === 0) return [];
let validNotifications = notifications; let validNotifications = notifications;
validNotifications = await this.#filterValidNotifier(validNotifications, meId);
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull); const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
const notes = noteIds.length > 0 ? await this.notesRepository.find({ const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) }, where: { id: In(noteIds) },
@ -269,7 +206,7 @@ export class NotificationEntityService implements OnModuleInit {
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p])); const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
// 既に解決されたフォローリクエストの通知を除外 // 既に解決されたフォローリクエストの通知を除外
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest'); const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<T, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
if (followRequestNotifications.length > 0) { if (followRequestNotifications.length > 0) {
const reqs = await this.followRequestsRepository.find({ const reqs = await this.followRequestsRepository.find({
where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) }, where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) },
@ -277,9 +214,107 @@ export class NotificationEntityService implements OnModuleInit {
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId)); validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
} }
return await Promise.all(validNotifications.map(x => this.packGrouped(x, meId, {}, { const packPromises = validNotifications.map(x => {
packedNotes, return this.pack(
packedUsers, x,
}))); meId,
{ checkValidNotifier: false },
{ packedNotes, packedUsers },
);
});
return (await Promise.all(packPromises)).filter(isNotNull);
}
@bindThis
public async pack(
src: MiNotification | MiGroupedNotification,
meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types
options: {
checkValidNotifier?: boolean;
},
hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
},
): Promise<Packed<'Notification'> | null> {
return await this.#packInternal(src, meId, options, hint);
}
@bindThis
public async packMany(
notifications: MiNotification[],
meId: MiUser['id'],
): Promise<MiNotification[]> {
return await this.#packManyInternal(notifications, meId);
}
@bindThis
public async packGroupedMany(
notifications: MiGroupedNotification[],
meId: MiUser['id'],
): Promise<MiGroupedNotification[]> {
return await this.#packManyInternal(notifications, meId);
}
/**
* notifierが存在するかvalidator
*/
#validateNotifier <T extends MiNotification | MiGroupedNotification> (
notification: T,
userIdsWhoMeMuting: Set<MiUser['id']>,
userMutedInstances: Set<string>,
notifiers: MiUser[],
): boolean {
if (!('notifierId' in notification)) return true;
if (userIdsWhoMeMuting.has(notification.notifierId)) return false;
const notifier = notifiers.find(x => x.id === notification.notifierId) ?? null;
if (notifier == null) return false;
if (notifier.host && userMutedInstances.has(notifier.host)) return false;
if (notifier.isSuspended) return false;
return true;
}
/**
* notifierが存在するか
*/
async #isValidNotifier(
notification: MiNotification | MiGroupedNotification,
meId: MiUser['id'],
): Promise<boolean> {
return (await this.#filterValidNotifier([notification], meId)).length === 1;
}
/**
* notifierが存在するか
*/
async #filterValidNotifier <T extends MiNotification | MiGroupedNotification> (
notifications: T[],
meId: MiUser['id'],
): Promise<T[]> {
const [
userIdsWhoMeMuting,
userMutedInstances,
] = await Promise.all([
this.cacheService.userMutingsCache.fetch(meId),
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
]);
const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull);
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
where: { id: In(notifierIds) },
}) : [];
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
return isValid ? notification : null;
}))) as [T | null] ).filter(isNotNull);
return filteredNotifications;
} }
} }

View File

@ -14,6 +14,7 @@ import type { MiPage } from '@/models/Page.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js';
@ -102,7 +103,7 @@ export class PageEntityService {
script: page.script, script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null)), attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)),
likedCount: page.likedCount, likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined, isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
}); });

View File

@ -25,6 +25,7 @@ import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
@ -384,7 +385,7 @@ export class UserEntityService implements OnModuleInit {
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null, movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
alsoKnownAs: user.alsoKnownAs alsoKnownAs: user.alsoKnownAs
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))) ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null) as string[]) .then(xs => xs.length === 0 ? null : xs.filter(isNotNull))
: null, : null,
createdAt: this.idService.parse(user.id).date.toISOString(), createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,

View File

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs/promises';
import type { PathLike } from 'node:fs';
/**
* `fs.createWriteStream()``WritableStream` (Web標準)
*/
export class FileWriterStream extends WritableStream<Uint8Array> {
constructor(path: PathLike) {
let file: fs.FileHandle | null = null;
super({
start: async () => {
file = await fs.open(path, 'a');
},
write: async (chunk, controller) => {
if (file === null) {
controller.error();
throw new Error();
}
await file.write(chunk);
},
close: async () => {
await file?.close();
},
abort: async () => {
await file?.close();
},
});
}
}

View File

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { TransformStream } from 'node:stream/web';
/**
* `JSON.stringify()`
*/
export class JsonArrayStream extends TransformStream<unknown, string> {
constructor() {
/** 最初の要素かどうかを変数に記録 */
let isFirst = true;
super({
start(controller) {
controller.enqueue('[');
},
flush(controller) {
controller.enqueue(']');
},
transform(chunk, controller) {
if (isFirst) {
isFirst = false;
} else {
// 妥当なJSON配列にするためには最初以外の要素の前に`,`を挿入しなければならない
controller.enqueue(',\n');
}
controller.enqueue(JSON.stringify(chunk));
},
});
}
}

View File

@ -186,28 +186,18 @@ export class RedisSingleCache<T> {
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
function nothingToDo<T, V = T>(value: T): V { export class MemoryKVCache<T> {
return value as unknown as V; /**
} *
* @deprecated
export class MemoryKVCache<T, V = T> { */
public cache: Map<string, { date: number; value: V; }>; public cache: Map<string, { date: number; value: T; }>;
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();
@ -215,10 +205,14 @@ export class MemoryKVCache<T, V = T> {
} }
@bindThis @bindThis
/**
* Mapにキャッシュをセットします
* @deprecated InternalEventなどで変更を全てのプロセス/
*/
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 +224,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 +235,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 +252,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 +260,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 +277,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

@ -3,8 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
// we are using {} as "any non-nullish value" as expected export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T {
// eslint-disable-next-line @typescript-eslint/ban-types
export function isNotNull<T extends {}>(input: T | undefined | null): input is T {
return input != null; return input != null;
} }

View File

@ -37,9 +37,25 @@ import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/jso
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,
packedRoleCondFormulaValueAssignedRoleSchema,
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';
import {
packedMetaLiteSchema,
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
} from '@/models/json-schema/meta.js';
export const refs = { export const refs = {
UserLite: packedUserLiteSchema, UserLite: packedUserLiteSchema,
@ -78,11 +94,21 @@ export const refs = {
EmojiDetailed: packedEmojiDetailedSchema, EmojiDetailed: packedEmojiDetailedSchema,
Flash: packedFlashSchema, Flash: packedFlashSchema,
Signin: packedSigninSchema, Signin: packedSigninSchema,
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
RoleCondFormulaValue: packedRoleCondFormulaValueSchema,
RoleLite: packedRoleLiteSchema, RoleLite: packedRoleLiteSchema,
Role: packedRoleSchema, Role: packedRoleSchema,
RolePolicies: packedRolePoliciesSchema, RolePolicies: packedRolePoliciesSchema,
ReversiGameLite: packedReversiGameLiteSchema, ReversiGameLite: packedReversiGameLiteSchema,
ReversiGameDetailed: packedReversiGameDetailedSchema, ReversiGameDetailed: packedReversiGameDetailedSchema,
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
}; };
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>; export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;

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

@ -144,4 +144,9 @@ export class MiInstance {
nullable: true, nullable: true,
}) })
public infoUpdatedAt: Date | null; public infoUpdatedAt: Date | null;
@Column('varchar', {
length: 16384, default: '',
})
public moderationNote: string;
} }

View File

@ -253,6 +253,8 @@ export class MiMeta {
}) })
public turnstileSecretKey: string | null; public turnstileSecretKey: string | null;
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
@Column('enum', { @Column('enum', {
enum: ['none', 'all', 'local', 'remote'], enum: ['none', 'all', 'local', 'remote'],
default: 'none', default: 'none',
@ -357,9 +359,9 @@ export class MiMeta {
@Column('varchar', { @Column('varchar', {
length: 1024, length: 1024,
default: 'https://github.com/misskey-dev/misskey', default: 'https://github.com/misskey-dev/misskey',
nullable: false, nullable: true,
}) })
public repositoryUrl: string; public repositoryUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 1024, length: 1024,

View File

@ -29,6 +29,11 @@ type CondFormulaValueIsRemote = {
type: 'isRemote'; type: 'isRemote';
}; };
type CondFormulaValueRoleAssignedTo = {
type: 'roleAssignedTo';
roleId: string;
};
type CondFormulaValueCreatedLessThan = { type CondFormulaValueCreatedLessThan = {
type: 'createdLessThan'; type: 'createdLessThan';
sec: number; sec: number;
@ -69,12 +74,13 @@ type CondFormulaValueNotesMoreThanOrEq = {
value: number; value: number;
}; };
export type RoleCondFormulaValue = export type RoleCondFormulaValue = { id: string } & (
CondFormulaValueAnd | CondFormulaValueAnd |
CondFormulaValueOr | CondFormulaValueOr |
CondFormulaValueNot | CondFormulaValueNot |
CondFormulaValueIsLocal | CondFormulaValueIsLocal |
CondFormulaValueIsRemote | CondFormulaValueIsRemote |
CondFormulaValueRoleAssignedTo |
CondFormulaValueCreatedLessThan | CondFormulaValueCreatedLessThan |
CondFormulaValueCreatedMoreThan | CondFormulaValueCreatedMoreThan |
CondFormulaValueFollowersLessThanOrEq | CondFormulaValueFollowersLessThanOrEq |
@ -82,7 +88,8 @@ export type RoleCondFormulaValue =
CondFormulaValueFollowingLessThanOrEq | CondFormulaValueFollowingLessThanOrEq |
CondFormulaValueFollowingMoreThanOrEq | CondFormulaValueFollowingMoreThanOrEq |
CondFormulaValueNotesLessThanOrEq | CondFormulaValueNotesLessThanOrEq |
CondFormulaValueNotesMoreThanOrEq; CondFormulaValueNotesMoreThanOrEq
);
@Entity('role') @Entity('role')
export class MiRole { export class MiRole {

View File

@ -249,6 +249,8 @@ export class MiUserProfile {
type: 'follower'; type: 'follower';
} | { } | {
type: 'mutualFollow'; type: 'mutualFollow';
} | {
type: 'followingOrFollower';
} | { } | {
type: 'list'; type: 'list';
userListId: MiUserList['id']; userListId: MiUserList['id'];

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