diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 0a21f20320..c6c83a98bf 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -56,17 +56,17 @@ dbReplications: false # You can configure any number of replicas here #dbSlaves: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -106,6 +106,9 @@ redis: # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── +# You can set scope to local (default value) or global +# (include notes from remote). + #meilisearch: # host: meilisearch # port: 7700 @@ -151,7 +154,7 @@ id: 'aidx' # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 16 +# inboxJobPerSec: 32 # relashionshipJobPerSec: 64 # Job attempts @@ -164,6 +167,9 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 +# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) +maxNoteLength: 3000 + # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 @@ -194,10 +200,17 @@ proxyRemoteFiles: true # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true +# check that inbound ActivityPub GET requests are signed ("authorized fetch") +checkActivityPubGetSignature: false +# For security reasons, uploading attachments from the intranet is prohibited, +# but exceptions can be made from the following settings. Default value is "undefined". +# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' #] +#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] + # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.config/example.yml b/.config/example.yml index 7cd710f250..4aa7757c61 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -118,6 +118,9 @@ redis: # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── +# You can set scope to local (default value) or global +# (include notes from remote). + #meilisearch: # host: localhost # port: 7700 @@ -163,7 +166,7 @@ id: 'aidx' # Job rate limiter #deliverJobPerSec: 128 -#inboxJobPerSec: 16 +#inboxJobPerSec: 32 #relashionshipJobPerSec: 64 # Job attempts @@ -176,6 +179,9 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 +# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) +maxNoteLength: 3000 + # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 @@ -209,11 +215,18 @@ proxyRemoteFiles: true # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true +# check that inbound ActivityPub GET requests are signed ("authorized fetch") +checkActivityPubGetSignature: false +# For security reasons, uploading attachments from the intranet is prohibited, +# but exceptions can be made from the following settings. Default value is "undefined". +# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' #] +#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] + # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0583a66960..e409adf644 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "version": "8.9.2" }, "ghcr.io/devcontainers/features/node:1": { - "version": "20.5.1" + "version": "20.10.0" } }, "forwardPorts": [3000], diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 3d57d1245d..7ea0929469 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -56,17 +56,17 @@ dbReplications: false # You can configure any number of replicas here #dbSlaves: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -147,7 +147,7 @@ id: 'aidx' # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 16 +# inboxJobPerSec: 32 # Job attempts # deliverJobMaxAttempts: 12 diff --git a/.forgejo/workflows/docker-develop.yml b/.forgejo/workflows/docker-develop.yml new file mode 100644 index 0000000000..0c8338c4df --- /dev/null +++ b/.forgejo/workflows/docker-develop.yml @@ -0,0 +1,58 @@ +name: Publish Docker image (develop) + +on: + push: + branches: + - develop + paths: + - packages/** + - locales/** + workflow_dispatch: + +env: + REGISTRY: git.joinsharkey.org + +jobs: + push_to_registry: + name: Push Docker image to GHCR + runs-on: docker + steps: + - name: install packages + run: apt-get update && apt-get install -y wget git curl + - uses: https://code.forgejo.org/actions/setup-node@v3 + with: + node-version: 20 + - name: Install docker + run: | + echo deb http://deb.debian.org/debian bullseye-backports main | tee /etc/apt/sources.list.d/backports.list && apt-get -qq update + DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -qq -y -t bullseye-backports docker.io + - name: Check out the repo + uses: actions/checkout@v4.1.1 + - name: Set up Docker Buildx + id: buildx + uses: https://github.com/docker/setup-buildx-action@v3.0.0 + with: + platforms: linux/amd64,linux/arm64 + - name: Docker meta + id: meta + uses: https://github.com/docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/sharkey/sharkey + - name: Log in to GHCR + uses: https://github.com/docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: Marie + password: ${{ secrets.TOKEN }} + - name: Build and Push to GHCR + id: build + uses: https://github.com/docker/build-push-action@v5 + with: + builder: ${{ steps.buildx.outputs.name }} + context: . + push: true + platforms: ${{ steps.buildx.outputs.platforms }} + provenance: false + tags: ${{ env.REGISTRY }}/sharkey/sharkey:develop + labels: develop + build-args: NODE_ENV=development \ No newline at end of file diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml new file mode 100644 index 0000000000..155c5f7deb --- /dev/null +++ b/.forgejo/workflows/docker.yml @@ -0,0 +1,62 @@ +name: Publish Docker image + +on: + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: git.joinsharkey.org + +jobs: + push_to_registry: + name: Push Docker image to GHCR + runs-on: docker + + steps: + - name: install packages + run: apt-get update && apt-get install -y wget git curl + - uses: https://code.forgejo.org/actions/setup-node@v3 + with: + node-version: 20 + - name: Install docker + run: | + echo deb http://deb.debian.org/debian bullseye-backports main | tee /etc/apt/sources.list.d/backports.list && apt-get -qq update + DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -qq -y -t bullseye-backports docker.io + - name: Check out the repo + uses: actions/checkout@v4.1.1 + - name: Set up Docker Buildx + id: buildx + uses: https://github.com/docker/setup-buildx-action@v3.0.0 + with: + platforms: linux/amd64,linux/arm64 + - name: Docker meta + id: meta + uses: https://github.com/docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/sharkey/sharkey + tags: | + type=edge + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=stable + - name: Log in to GHCR + uses: https://github.com/docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: Marie + password: ${{ secrets.TOKEN }} + - name: Build and Push to GHCR + id: build + uses: https://github.com/docker/build-push-action@v5 + with: + builder: ${{ steps.buildx.outputs.name }} + context: . + push: true + platforms: ${{ steps.buildx.outputs.platforms }} + provenance: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.forgejo/workflows/lint.yml similarity index 74% rename from .github/workflows/lint.yml rename to .forgejo/workflows/lint.yml index c63ccc87fc..0a773d5fb0 100644 --- a/.github/workflows/lint.yml +++ b/.forgejo/workflows/lint.yml @@ -3,27 +3,25 @@ name: Lint on: push: branches: - - master + - stable - develop paths: - packages/** pull_request: - branches-ignore: - - weblate jobs: pnpm_install: - runs-on: ubuntu-latest + runs-on: docker steps: - uses: actions/checkout@v4.1.1 with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v2 + - uses: https://github.com/pnpm/action-setup@v2 with: version: 8 run_install: false - - uses: actions/setup-node@v4 + - uses: https://code.forgejo.org/actions/setup-node@v4 with: node-version-file: '.node-version' cache: 'pnpm' @@ -32,7 +30,7 @@ jobs: lint: needs: [pnpm_install] - runs-on: ubuntu-latest + runs-on: docker continue-on-error: true strategy: matrix: @@ -46,11 +44,11 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v2 + - uses: https://github.com/pnpm/action-setup@v2 with: version: 7 run_install: false - - uses: actions/setup-node@v4 + - uses: https://code.forgejo.org/actions/setup-node@v4 with: node-version-file: '.node-version' cache: 'pnpm' @@ -60,7 +58,7 @@ jobs: typecheck: needs: [pnpm_install] - runs-on: ubuntu-latest + runs-on: docker continue-on-error: true strategy: matrix: @@ -72,14 +70,16 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: pnpm/action-setup@v2 + - uses: https://github.com/pnpm/action-setup@v2 with: version: 7 run_install: false - - uses: actions/setup-node@v4 + - uses: https://code.forgejo.org/actions/setup-node@v4 with: node-version-file: '.node-version' cache: 'pnpm' - run: corepack enable - run: pnpm i --frozen-lockfile + - run: pnpm --filter misskey-js run build + if: ${{ matrix.workspace == 'backend' }} - run: pnpm --filter ${{ matrix.workspace }} run typecheck diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.gitea/ISSUE_TEMPLATE/01_bug-report.yml similarity index 93% rename from .github/ISSUE_TEMPLATE/01_bug-report.yml rename to .gitea/ISSUE_TEMPLATE/01_bug-report.yml index f74719989f..6282cc43f9 100644 --- a/.github/ISSUE_TEMPLATE/01_bug-report.yml +++ b/.gitea/ISSUE_TEMPLATE/01_bug-report.yml @@ -1,6 +1,6 @@ name: 🐛 Bug Report description: Create a report to help us improve -labels: ["⚠️bug?"] +title: 'bug: ' body: - type: markdown @@ -89,3 +89,9 @@ body: render: markdown validations: required: false + + - type: checkboxes + attributes: + label: Do you want to address this bug yourself? + options: + - label: Yes, I will patch the bug myself and send a pull request diff --git a/.github/ISSUE_TEMPLATE/02_feature-request.yml b/.gitea/ISSUE_TEMPLATE/02_feature-request.yml similarity index 64% rename from .github/ISSUE_TEMPLATE/02_feature-request.yml rename to .gitea/ISSUE_TEMPLATE/02_feature-request.yml index 8420475b3e..d3bf64d869 100644 --- a/.github/ISSUE_TEMPLATE/02_feature-request.yml +++ b/.gitea/ISSUE_TEMPLATE/02_feature-request.yml @@ -1,6 +1,6 @@ name: ✨ Feature Request description: Suggest an idea for this project -labels: ["✨Feature"] +title: 'feat: ' body: - type: textarea @@ -14,4 +14,9 @@ body: label: Purpose description: Describe the specific problem or need you think this feature will solve, and who it will help. validations: - required: true \ No newline at end of file + required: true + - type: checkboxes + attributes: + label: Do you want to implement this feature yourself? + options: + - label: Yes, I will implement this by myself and send a pull request \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.gitea/ISSUE_TEMPLATE/config.yml similarity index 69% rename from .github/ISSUE_TEMPLATE/config.yml rename to .gitea/ISSUE_TEMPLATE/config.yml index 6c8c6f7a49..b845c1c9ac 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.gitea/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ contact_links: - name: 💬 Transfem.org Discord - url: https://discord.gg/xTtXc7We + url: https://discord.gg/HJcAanTR6H about: Chat freely about Sharkey diff --git a/.github/pull_request_template.md b/.gitea/pull_request_template.md similarity index 96% rename from .github/pull_request_template.md rename to .gitea/pull_request_template.md index e78b82c47c..63eb2ab623 100644 --- a/.github/pull_request_template.md +++ b/.gitea/pull_request_template.md @@ -3,22 +3,22 @@ PRありがとうございます! PRを作成する前に、コントリビュ Thank you for your PR! Before creating a PR, please check the contribution guide: https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md --> - + ## What - + ## Why - + ## Additional info (optional) - + ## Checklist - [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) - [ ] Test working in a local environment - [ ] (If needed) Add story of storybook - [ ] (If needed) Update CHANGELOG.md -- [ ] (If possible) Add tests +- [ ] (If possible) Add tests \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 2383856700..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms - -github: transfem-org -ko-fi: transfem diff --git a/.github/PULL_REQUEST_TEMPLATE/01_bug.md b/.github/PULL_REQUEST_TEMPLATE/01_bug.md deleted file mode 100644 index 0739fee709..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE/01_bug.md +++ /dev/null @@ -1,23 +0,0 @@ - - -## What - - - -## Why - - - -## Additional info (optional) - - - -## Checklist -- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) -- [ ] Test working in a local environment -- [ ] (If needed) Update CHANGELOG.md -- [ ] (If possible) Add tests diff --git a/.github/PULL_REQUEST_TEMPLATE/02_enhance.md b/.github/PULL_REQUEST_TEMPLATE/02_enhance.md deleted file mode 100644 index 0739fee709..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE/02_enhance.md +++ /dev/null @@ -1,23 +0,0 @@ - - -## What - - - -## Why - - - -## Additional info (optional) - - - -## Checklist -- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) -- [ ] Test working in a local environment -- [ ] (If needed) Update CHANGELOG.md -- [ ] (If possible) Add tests diff --git a/.github/PULL_REQUEST_TEMPLATE/03_release.md b/.github/PULL_REQUEST_TEMPLATE/03_release.md deleted file mode 100644 index b5b832e1dc..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE/03_release.md +++ /dev/null @@ -1,20 +0,0 @@ -## Summary -This is a release PR. - -For more information on the release instructions, please see: -https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md#release - -## For reviewers -- CHANGELOGに抜け漏れは無いか -- バージョンの上げ方は適切か -- 他にこのリリースに含めなければならない変更は無いか -- 全体的な変更内容を俯瞰し問題は無いか -- レビューされていないコミットがある場合は、それが問題ないか -- 最終的な動作確認を行い問題は無いか - -などを確認し、リリースする準備が整っていると思われる場合は approve してください。 - -## Checklist -- [ ] package.jsonのバージョンが正しく更新されている -- [ ] CHANGELOGが過不足無く更新されている -- [ ] CIが全て通っている diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 5955f6b5d9..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,32 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 100 - -# Add only the root, not each workspace item -# https://github.com/dependabot/dependabot-core/issues/4993#issuecomment-1289133027 -- package-ecosystem: npm - directory: "/" - schedule: - interval: daily - # PNPM has an issue with dependabot. See: - # https://github.com/dependabot/dependabot-core/issues/7258 - # https://github.com/pnpm/pnpm/issues/6530 - # TODO: Restore this when the issue is solved - open-pull-requests-limit: 0 - groups: - swc: - patterns: - - "@swc/*" - storybook: - patterns: - - "storybook*" - - "@storybook/*" diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 137be487c0..0000000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,21 +0,0 @@ -'packages/backend': -- packages/backend/**/* - -'packages/backend:test': -- packages/backend/test/**/* - -'packages/frontend': -- packages/frontend/**/* - -'packages/frontend:test': -- cypress/**/* - -'packages/sw': -- packages/sw/**/* - -'packages/misskey-js': -- packages/misskey-js/**/* - -'packages/misskey-js:test': -- packages/misskey-js/test/**/* -- packages/misskey-js/test-d/**/* diff --git a/.github/misskey/test.yml b/.github/misskey/test.yml deleted file mode 100644 index 7a4aa4ae6c..0000000000 --- a/.github/misskey/test.yml +++ /dev/null @@ -1,15 +0,0 @@ -url: 'http://misskey.local' - -# ローカルでテストするときにポートを被らないようにするためデフォルトのものとは変える(以下同じ) -port: 61812 - -db: - host: 127.0.0.1 - port: 54312 - db: test-misskey - user: postgres - pass: '' -redis: - host: 127.0.0.1 - port: 56312 -id: aidx diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml deleted file mode 100644 index 7818370c0a..0000000000 --- a/.github/workflows/api-misskey-js.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: API report (misskey.js) - -on: [push, pull_request] - -jobs: - report: - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - - - run: corepack enable - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm i --frozen-lockfile - - - name: Build - run: pnpm --filter misskey-js build - - - name: Check files - run: ls packages/misskey-js/built - - - name: API report - run: pnpm --filter misskey-js api-prod - - - name: Show report - if: always() - run: cat packages/misskey-js/temp/misskey-js.api.md diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml deleted file mode 100644 index 03dfcd0a0b..0000000000 --- a/.github/workflows/check_copyright_year.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Check copyright year - -on: - push: - branches: - - master - - develop - -jobs: - check_copyright_year: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.1.1 - - run: | - if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then - echo "Please change copyright year!" - exit 1 - fi diff --git a/.github/workflows/clear-untagged-packages.yml b/.github/workflows/clear-untagged-packages.yml deleted file mode 100644 index 5f94d54bcb..0000000000 --- a/.github/workflows/clear-untagged-packages.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Remove old package versions - -on: - workflow_dispatch: - -jobs: - remove-package-versions: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - uses: actions/delete-package-versions@v4 - with: - package-name: sharkey - package-type: container - min-versions-to-keep: 10 - delete-only-untagged-versions: 'true' diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml deleted file mode 100644 index e5e8392d78..0000000000 --- a/.github/workflows/docker-develop.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Publish Docker image (develop) - -on: - push: - branches: - - develop - paths: - - packages/** - - locales/** - workflow_dispatch: - -env: - REGISTRY: ghcr.io - -jobs: - push_to_registry: - name: Push Docker image to GHCR - runs-on: ubuntu-latest - if: github.repository == 'transfem-org/Sharkey' - permissions: - contents: read - packages: write - steps: - - name: Remove unnecessary files - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - 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: ${{ env.REGISTRY }}/transfem-org/sharkey - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and Push to GHCR - id: build - uses: docker/build-push-action@v5 - with: - builder: ${{ steps.buildx.outputs.name }} - context: . - push: true - platforms: ${{ steps.buildx.outputs.platforms }} - provenance: false - tags: ${{ env.REGISTRY }}/transfem-org/sharkey:develop - labels: develop - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: NODE_ENV=development - - name: Push update to server - if: steps.build.outcome == 'success' - uses: indiesdev/curl@v1.1 - with: - url: ${{ secrets.AUTO_UPDATE_DEV_URL }} - method: POST - timeout: 600000 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 8eabaa2882..0000000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Publish Docker image - -on: - push: - branches: - - stable - paths: - - packages/** - - locales/** - release: - types: [published] - workflow_dispatch: - -env: - REGISTRY: ghcr.io - -jobs: - push_to_registry: - name: Push Docker image to GHCR - runs-on: ubuntu-latest - if: github.repository == 'transfem-org/Sharkey' - permissions: - contents: read - packages: write - - 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: ${{ env.REGISTRY }}/transfem-org/sharkey - tags: | - type=edge - type=ref,event=pr - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and Push to GHCR - id: build - uses: docker/build-push-action@v5 - with: - builder: ${{ steps.buildx.outputs.name }} - context: . - push: true - platforms: ${{ steps.buildx.outputs.platforms }} - provenance: false - tags: ${{ env.REGISTRY }}/transfem-org/sharkey:stable - labels: stable - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml deleted file mode 100644 index edb18b04da..0000000000 --- a/.github/workflows/dockle.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Dockle - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - dockle: - runs-on: ubuntu-latest - env: - DOCKER_CONTENT_TRUST: 1 - steps: - - uses: actions/checkout@v4.1.1 - - run: | - curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" - sudo dpkg -i dockle.deb - - run: | - cp .config/docker_example.env .config/docker.env - cp ./docker-compose.yml.example ./docker-compose.yml - - run: | - docker compose up -d web - docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest - - run: | - cmd="dockle --exit-code 1 misskey-web:latest ${image_name}" - echo "> ${cmd}" - eval "${cmd}" diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml deleted file mode 100644 index 8e3437ad86..0000000000 --- a/.github/workflows/get-api-diff.yml +++ /dev/null @@ -1,186 +0,0 @@ -# this name is used in report-api-diff.yml so be careful when change name -name: Get api.json from Misskey - -on: - pull_request: - branches: - - master - - develop - -jobs: - get-base: - runs-on: ubuntu-latest - permissions: - contents: read - - strategy: - matrix: - node-version: [20.5.1] - - services: - db: - image: postgres:13 - ports: - - 5432:5432 - env: - POSTGRES_DB: misskey - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_USER: example-misskey-user - POSTGRESS_PASS: example-misskey-pass - redis: - image: redis:7 - ports: - - 6379:6379 - - steps: - - uses: actions/checkout@v4.1.1 - with: - repository: ${{ github.event.pull_request.base.repo.full_name }} - ref: ${{ github.base_ref }} - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - run_install: false - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.0 - with: - node-version: ${{ matrix.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: Copy Configure - run: cp .config/example.yml .config/default.yml - - name: Build - run: pnpm build - - name : Migrate - run: pnpm migrate - - name: Launch misskey - run: | - screen -S misskey -dm pnpm run dev - sleep 30s - - name: Wait for Misskey to be ready - run: | - MAX_RETRIES=12 - RETRY_DELAY=5 - count=0 - until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do - printf '.' - sleep $RETRY_DELAY - count=$((count + 1)) - done - - if [[ $count -eq $MAX_RETRIES ]]; then - echo "Failed to connect to Misskey after $MAX_RETRIES attempts." - exit 1 - fi - - id: fetch - name: Get api.json from Misskey - run: | - RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json) - echo $RESULT > api-base.json - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - name: api-artifact - path: api-base.json - - name: Kill Misskey Job - run: screen -S misskey -X quit - - get-head: - runs-on: ubuntu-latest - permissions: - contents: read - - strategy: - matrix: - node-version: [20.5.1] - - services: - db: - image: postgres:13 - ports: - - 5432:5432 - env: - POSTGRES_DB: misskey - POSTGRES_HOST_AUTH_METHOD: trust - POSTGRES_USER: example-misskey-user - POSTGRESS_PASS: example-misskey-pass - redis: - image: redis:7 - ports: - - 6379:6379 - - steps: - - uses: actions/checkout@v4.1.1 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.head_ref }} - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - run_install: false - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.0 - with: - node-version: ${{ matrix.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: Copy Configure - run: cp .config/example.yml .config/default.yml - - name: Build - run: pnpm build - - name : Migrate - run: pnpm migrate - - name: Launch misskey - run: | - screen -S misskey -dm pnpm run dev - sleep 30s - - name: Wait for Misskey to be ready - run: | - MAX_RETRIES=12 - RETRY_DELAY=5 - count=0 - until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do - printf '.' - sleep $RETRY_DELAY - count=$((count + 1)) - done - - if [[ $count -eq $MAX_RETRIES ]]; then - echo "Failed to connect to Misskey after $MAX_RETRIES attempts." - exit 1 - fi - - id: fetch - name: Get api.json from Misskey - run: | - RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json) - echo $RESULT > api-head.json - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - name: api-artifact - path: api-head.json - - name: Kill Misskey Job - run: screen -S misskey -X quit - - save-pr-number: - runs-on: ubuntu-latest - steps: - - name: Save PR number - env: - PR_NUMBER: ${{ github.event.number }} - run: | - echo "$PR_NUMBER" > ./pr_number - - uses: actions/upload-artifact@v3 - with: - name: api-artifact - path: pr_number diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index fa4a58c3a9..0000000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: "Pull Request Labeler" -on: - pull_request_target: - branches-ignore: - - 'l10n_develop' - -jobs: - triage: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/labeler@v4 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml deleted file mode 100644 index c02b980e4d..0000000000 --- a/.github/workflows/ok-to-test.yml +++ /dev/null @@ -1,36 +0,0 @@ -# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event -name: Ok To Test - -on: - issue_comment: - types: [created] - -jobs: - ok-to-test: - runs-on: ubuntu-latest - # Only run for PRs, not issue comments - if: ${{ github.event.issue.pull_request }} - steps: - # Generate a GitHub App installation access token from an App ID and private key - # To create a new GitHub App: - # https://developer.github.com/apps/building-github-apps/creating-a-github-app/ - # See app.yml for an example app manifest - - name: Generate token - id: generate_token - uses: tibdex/github-app-token@v2 - with: - app_id: ${{ secrets.DEPLOYBOT_APP_ID }} - private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }} - - - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v3 - env: - TOKEN: ${{ steps.generate_token.outputs.token }} - with: - token: ${{ env.TOKEN }} # GitHub App installation access token - # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work - reaction-token: ${{ secrets.GITHUB_TOKEN }} - issue-type: pull-request - commands: deploy - named-args: true - permission: write diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml deleted file mode 100644 index d10d0fcafd..0000000000 --- a/.github/workflows/package.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Publish prebuild - -on: - push: - branches: - - stable - release: - types: [published] - workflow_dispatch: - -jobs: - build_binaries: - name: Build & ship binaries - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [20.x] - python-version: [3.11.x] - if: github.repository == 'transfem-org/Sharkey' - permissions: - contents: read - packages: write - - steps: - - name: Check out the repo - uses: actions/checkout@v4.1.1 - with: - lfs: true - submodules: 'recursive' - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Setup Python - uses: actions/setup-python@v4.7.1 - with: - python-version: ${{ matrix.python-version }} - - - name: Cache APT Packages - uses: awalsh128/cache-apt-pkgs-action@v1.3.1 - with: - packages: "build-essential binfmt-support qemu-user-static ffmpeg tini curl libjemalloc-dev libjemalloc2 uuid-dev libx11-dev libxkbfile-dev execstack libgconf-2-4 libsecret-1-dev" - - - name: Set pnpm store path - run: echo "PNPM_STORE_PATH=$(pnpm store path)" >> $GITHUB_ENV - - - name: Cache node modules - uses: actions/cache@v3 - with: - path: ${{ env.PNPM_STORE_PATH }} - key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - pnpm-${{ runner.os }}- - - - name: Build - run: | - corepack enable - corepack prepare pnpm@latest --activate - CI=true pnpm install - CI=true pnpm run build - rm -rdf packages/backend/node_modules - rm -rdf packages/frontend/node_modules - rm -rdf packages/megalodon/node_modules - rm -rdf packages/misskey-js/node_modules - rm -rdf node_modules - CI=true pnpm --prod --no-optional install - tar -czf /tmp/workspace.tar.gz . - - name: Upload linux x64 - uses: actions/upload-artifact@v3.1.3 - with: - name: sharkey-linux-x64 - path: /tmp/workspace.tar.gz diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml deleted file mode 100644 index cb9a4ebfc8..0000000000 --- a/.github/workflows/pr-preview-deploy.yml +++ /dev/null @@ -1,92 +0,0 @@ -# Run secret-dependent integration tests only after /deploy approval -on: - repository_dispatch: - types: [deploy-command] - -name: Deploy preview environment - -jobs: - # Repo owner has commented /deploy on a (fork-based) pull request - deploy-preview-environment: - runs-on: ubuntu-latest - if: - github.event.client_payload.slash_command.sha != '' && - contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) - steps: - - uses: actions/github-script@v7 - id: check-id - env: - number: ${{ github.event.client_payload.pull_request.number }} - job: ${{ github.job }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - result-encoding: string - script: | - const { data: pull } = await github.rest.pulls.get({ - ...context.repo, - pull_number: process.env.number - }); - const ref = pull.head.sha; - - const { data: checks } = await github.rest.checks.listForRef({ - ...context.repo, - ref - }); - - const check = checks.check_runs.filter(c => c.name === process.env.job); - - return check[0].id; - - - uses: actions/github-script@v7 - env: - check_id: ${{ steps.check-id.outputs.result }} - details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.checks.update({ - ...context.repo, - check_run_id: process.env.check_id, - status: 'in_progress', - details_url: process.env.details_url - }); - - # Check out merge commit - - name: Fork based /deploy checkout - uses: actions/checkout@v4.1.1 - with: - ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' - - # - - name: Context - uses: okteto/context@latest - with: - token: ${{ secrets.OKTETO_TOKEN }} - - - name: Deploy preview environment - uses: ikuradon/deploy-preview@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo - timeout: 15m - - # Update check run called "integration-fork" - - uses: actions/github-script@v7 - id: update-check-run - if: ${{ always() }} - env: - # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run - conclusion: ${{ job.status }} - check_id: ${{ steps.check-id.outputs.result }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { data: result } = await github.rest.checks.update({ - ...context.repo, - check_run_id: process.env.check_id, - status: 'completed', - conclusion: process.env.conclusion - }); - - return result; diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml deleted file mode 100644 index 47d9eb313a..0000000000 --- a/.github/workflows/pr-preview-destroy.yml +++ /dev/null @@ -1,54 +0,0 @@ -# file: .github/workflows/preview-closed.yaml -on: - pull_request: - types: - - closed - -name: Destroy preview environment - -jobs: - destroy-preview-environment: - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v7 - id: check-conclusion - env: - number: ${{ github.event.number }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - result-encoding: string - script: | - const { data: pull } = await github.rest.pulls.get({ - ...context.repo, - pull_number: process.env.number - }); - const ref = pull.head.sha; - - const { data: checks } = await github.rest.checks.listForRef({ - ...context.repo, - ref - }); - - const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment'); - - if (check.length === 0) { - return; - } - - const { data: result } = await github.rest.checks.get({ - ...context.repo, - check_run_id: check[0].id, - }); - - return result.conclusion; - - name: Context - if: steps.check-conclusion.outputs.result == 'success' - uses: okteto/context@latest - with: - token: ${{ secrets.OKTETO_TOKEN }} - - - name: Destroy preview environment - if: steps.check-conclusion.outputs.result == 'success' - uses: okteto/destroy-preview@latest - with: - name: pr-${{ github.event.number }}-syuilo diff --git a/.github/workflows/report-api-diff.yml b/.github/workflows/report-api-diff.yml deleted file mode 100644 index 2868d6cc09..0000000000 --- a/.github/workflows/report-api-diff.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Report API Diff - -on: - workflow_run: - types: [completed] - workflows: - - Get api.json from Misskey # get-api-diff.yml - -jobs: - compare-diff: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} - permissions: - pull-requests: write - -# api-artifact - steps: - - name: Download artifact - uses: actions/github-script@v7 - with: - script: | - let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { - return artifact.name == "api-artifact" - })[0]; - let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - let fs = require('fs'); - fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/api-artifact.zip`, Buffer.from(download.data)); - - name: Extract artifact - run: unzip api-artifact.zip -d artifacts - - name: Load PR Number - id: load-pr-num - run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT" - - - name: Output base - run: cat ./artifacts/api-base.json - - name: Output head - run: cat ./artifacts/api-head.json - - name: Arrange json files - run: | - jq '.' ./artifacts/api-base.json > ./api-base.json - jq '.' ./artifacts/api-head.json > ./api-head.json - - name: Get diff of 2 files - run: diff -u --label=base --label=head ./api-base.json ./api-head.json | cat > api.json.diff - - name: Get full diff - run: diff --label=base --label=head --new-line-format='+%L' --old-line-format='-%L' --unchanged-line-format=' %L' ./api-base.json ./api-head.json | cat > api-full.json.diff - - name: Echo full diff - run: cat ./api-full.json.diff - - name: Upload full diff to Artifact - uses: actions/upload-artifact@v3 - with: - name: api-artifact - path: | - api-full.json.diff - api-base.json - api-head.json - - id: out-diff - name: Build diff Comment - run: | - cat <<- EOF > ./output.md - このPRによるapi.jsonの差分 -
- 差分はこちら - - \`\`\`diff - $(cat ./api.json.diff) - \`\`\` -
- - [Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) - EOF - - uses: thollander/actions-comment-pull-request@v2 - with: - pr_number: ${{ steps.load-pr-num.outputs.pr-number }} - comment_tag: show_diff - filePath: ./output.md diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml deleted file mode 100644 index 711e8bb7f5..0000000000 --- a/.github/workflows/test-backend.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Test (backend) - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - jest: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.5.1] - - services: - postgres: - image: postgres:13 - ports: - - 54312:5432 - env: - POSTGRES_DB: test-misskey - POSTGRES_HOST_AUTH_METHOD: trust - redis: - image: redis:7 - ports: - - 56312:6379 - - steps: - - uses: actions/checkout@v4.1.1 - with: - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - run_install: false - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.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: Copy Configure - run: cp .github/misskey/test.yml .config - - name: Build - run: pnpm build - - name: Test - run: pnpm jest-and-coverage - - name: Upload Coverage - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./packages/backend/coverage/coverage-final.json diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml deleted file mode 100644 index 62b2fe3e38..0000000000 --- a/.github/workflows/test-frontend.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: Test (frontend) - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - vitest: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.5.1] - - steps: - - uses: actions/checkout@v4.1.1 - with: - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - run_install: false - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.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: Copy Configure - run: cp .github/misskey/test.yml .config - - name: Build - run: pnpm build - - name: Test - run: pnpm --filter frontend test-and-coverage - - name: Upload Coverage - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./packages/frontend/coverage/coverage-final.json - - e2e: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - node-version: [20.5.1] - browser: [chrome] - - services: - postgres: - image: postgres:13 - ports: - - 54312:5432 - env: - POSTGRES_DB: test-misskey - POSTGRES_HOST_AUTH_METHOD: trust - redis: - image: redis:7 - ports: - - 56312:6379 - - steps: - - uses: actions/checkout@v4.1.1 - with: - submodules: true - # https://github.com/cypress-io/cypress-docker-images/issues/150 - #- name: Install mplayer for FireFox - # run: sudo apt install mplayer -y - # if: ${{ matrix.browser == 'firefox' }} - #- uses: browser-actions/setup-firefox@latest - # if: ${{ matrix.browser == 'firefox' }} - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 7 - run_install: false - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - name: Copy Configure - run: cp .github/misskey/test.yml .config - - name: Build - run: pnpm build - # https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091 - - name: ALSA Env - run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc - # XXX: This tries reinstalling Cypress if the binary is not cached - # Remove this when the cache issue is fixed - - name: Cypress install - run: pnpm exec cypress install - - name: Cypress run - uses: cypress-io/github-action@v6 - with: - install: false - start: pnpm start:test - wait-on: 'http://localhost:61812' - headed: true - browser: ${{ matrix.browser }} - - uses: actions/upload-artifact@v3 - if: failure() - with: - name: ${{ matrix.browser }}-cypress-screenshots - path: cypress/screenshots - - uses: actions/upload-artifact@v3 - if: always() - with: - name: ${{ matrix.browser }}-cypress-videos - path: cypress/videos diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml deleted file mode 100644 index 36816c78b8..0000000000 --- a/.github/workflows/test-misskey-js.yml +++ /dev/null @@ -1,52 +0,0 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Test (misskey.js) - -on: - push: - branches: [ develop ] - pull_request: - branches: [ develop ] - -jobs: - test: - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.5.1] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - - - run: corepack enable - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - - - name: Install dependencies - run: pnpm i --frozen-lockfile - - - name: Check pnpm-lock.yaml - run: git diff --exit-code pnpm-lock.yaml - - - name: Build - run: pnpm --filter misskey-js build - - - name: Test - run: pnpm --filter misskey-js test - env: - CI: true - - - name: Upload Coverage - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./packages/misskey-js/coverage/coverage-final.json diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml deleted file mode 100644 index 6b98fc51e6..0000000000 --- a/.github/workflows/test-production.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Test (production install and build) - -on: - push: - branches: - - master - - develop - pull_request: - -env: - NODE_ENV: production - -jobs: - production: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.5.1] - - steps: - - uses: actions/checkout@v4.1.1 - with: - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - run_install: false - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.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: Copy Configure - run: cp .github/misskey/test.yml .config/default.yml - - name: Build - run: pnpm build diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml deleted file mode 100644 index fb6c9ebdff..0000000000 --- a/.github/workflows/welcome.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Welcome -on: - pull_request: - types: [opened] - issues: - types: [opened] -jobs: - run: - runs-on: ubuntu-latest - steps: - - uses: actions/first-interaction@v1.3.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: | - 👋 @{{ author }} - Thanks for opening your first issue here! If you are reporting a bug, please make sure to include steps on how to reproduce it! :D - - pr-message: | - 👋 @{{ author }} - Thanks for opening this pull request! We will review it as soon as we can :3 - Please check out our contributing guidelines in the meantime. - - # FIRST_PR_MERGED: | - # 🎉 @{{ author }} - # Congrats on getting your first pull request merged! We are proud of you :3 ❤️ diff --git a/.node-version b/.node-version index 7cc2069986..d5a159609d 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.5.1 +20.10.0 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..da702c3af5 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@sharkey:registry=https://git.joinsharkey.org/api/packages/Sharkey/npm/ +engine-strict = true diff --git a/.vscode/settings.json b/.vscode/settings.json index 71fb02a59d..e2a82b1ffe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,15 @@ { - "search.exclude": { - "**/node_modules": true - }, - "typescript.tsdk": "node_modules/typescript/lib", - "files.associations": { - "*.test.ts": "typescript" - }, - "jest.jestCommandLine": "pnpm run jest", - "jest.autoRun": "off" -} \ No newline at end of file + "search.exclude": { + "**/node_modules": true + }, + "typescript.tsdk": "node_modules/typescript/lib", + "files.associations": { + "*.test.ts": "typescript" + }, + "jest.jestCommandLine": "pnpm run jest", + "jest.autoRun": "off", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "editor.formatOnSave": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 92e02508fe..30e2e57b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,156 @@ - ### Client -- +- ### Server - --> +## 2023.12.2 + +### General +- v2023.12.1でDockerを利用してサーバーを起動できない問題を修正 + +### Client +- Enhance: 検索画面においてEnterキー押下で検索できるように + +## 2023.12.1 + +### Note +- アクセストークンの権限が再整理されたため、一部のAPIが古いAPIトークンでは動作しなくなりました。\ + 権限不足になる場合には権限を再設定して再生成してください。 + +### General +- Enhance: ローカリゼーションの更新 +- Fix: 自分のdirect noteがuser list timelineに追加されない + +### Client +- Feat: AiScript専用のMFM構文`$[clickable.ev=EVENTNAME ...]`を追加。`Mk:C:mfm`のオプション`onClickEv`に関数を渡すと、クリック時に`EVENTNAME`を引数にして呼び出す +- Enhance: MFM入力補助ボタンを投稿フォームに表示できるように #12787 +- Fix: 一部のモデログ(logYellowでの表示対象)について、表示の色が変わらない問題を修正 +- Fix: `fg`/`bg`MFMに長い単語を指定すると、オーバーフローされずはみ出る問題を修正 + +### Server +- Enhance: センシティブワードの設定がハッシュタグトレンドにも適用されるようになりました +- Enhance: `oauth/token`エンドポイントのCORS対応 +- Fix: 1702718871541-ffVisibility.jsのdownが壊れている +- Fix:「非センシティブのみ(リモートはいいねのみ)」を設定していても、センシティブに設定されたカスタム絵文字をリアクションできる問題を修正 +- Fix: ロールアサイン時の通知で,ロールアイコンが縮小されずに表示される問題を修正 +- Fix: サードパーティアプリケーションがWebsocket APIに無条件にアクセスできる問題を修正 +- Fix: サードパーティアプリケーションがユーザーの許可なしに非公開の情報を見ることができる問題を修正 + +## 2023.12.0 + +### Note +- 依存関係の更新に伴い、Node.js 20.10.0が最小要件になりました +- 絵文字の追加辞書を既にインストールしている場合は、お手数ですが再インストールのほどお願いします +- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。 + + **影響:** + それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された"ピン留め(全般)"の設定が使われるため)。 + 投稿用のピン留め絵文字をアップデート前の状態にするには、以下の手順で操作します。 + + 1. 「設定」メニューに移動し、「絵文字ピッカー」タブを選択します。 + 2. 「ピン留 (全般)」のタブを選択します。 + 3. 「リアクション設定から上書きする」ボタンを押すことで、アップデート前の状態に戻すことができます。 + +### General +- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) +- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) +- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 +- Enhance: 指定したドメインのメールアドレスの登録を弾くことができるように +- Enhance: 公開ロールにアサインされたときに通知が作成されるように +- Enhance: アイコンデコレーションを複数設定できるように +- Enhance: アイコンデコレーションの位置を微調整できるように +- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072 +- Enhance: ローカリゼーションの更新 +- Enhance: 依存関係の更新 +- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 + +### Client +- Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加 +- Feat: 画面に雪を降らせられるように +- Enhance: MFMのアニメーション要素(`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)に `delay` オプションを追加 +- Enhance: センシティブと判断されたウェブサイトのサムネイルを非表示に + - ウェブサイトをセンシティブと判断する仕組みが動いていないため、summalyProxyを使用しないと機能しません。 +- Enhance: 投稿フォームの絵文字ピッカーをリアクション時に使用するものと同じのを使用するように #12336 #12560 +- Enhance: リアクション用ピン留め絵文字と投稿時の絵文字入力用ピン留め絵文字を分けて設定できるように #12560 +- Enhance: 絵文字のオートコンプリート機能強化 #12364 +- Enhance: ユーザーのRawデータを表示するページが復活 +- Enhance: リアクション選択時に音を鳴らせるように +- Enhance: サウンドにドライブのファイルを使用できるように +- Enhance: ナビゲーションバーに項目「キャッシュを削除」を追加 +- Enhance: Shareページで投稿を完了すると、親ウィンドウ(親フレーム)にpostMessageするように +- Enhance: チャンネル、クリップ、ページ、Play、ギャラリーにURLのコピーボタンを設置 #11305 +- Enhance: ノートプレビューに「内容を隠す」が反映されるように +- Enhance: データセーバーでコードハイライトの読み込みを削減できるように +- Enhance: データセーバーの適用範囲を個別で設定できるように + - 従来のデータセーバーの設定はリセットされます +- Enhance: タイムライン上のタブからリスト、アンテナ、チャンネルの管理ページにジャンプできるように +- Enhance: ユーザー名、プロフィール、お知らせ、ページの編集画面でMFMや絵文字のオートコンプリートが使用できるように +- Enhance: プロフィール、お知らせの編集画面でMFMのプレビューを表示できるように +- Enhance: 絵文字の詳細ページに記載される情報を追加 +- Enhance: リアクションの表示幅制限を設定可能に +- Enhance: Unicode 15.0のサポート +- Enhance: コードブロックのハイライト機能を利用するには言語を明示的に指定させるように + - MFMでコードブロックを利用する際に意図しないハイライトが起こらないようになりました + - 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります + (例: ` ```js ` → Javascript, ` ```ais ` → AiScript) +- Enhance: 絵文字などのオートコンプリートでShift+Tabを押すと前の候補を選択できるように +- Enhance: チャンネルに新規の投稿がある場合にバッジを表示させる +- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加 +- Enhance: 設定したタグをトレンドに表示させないようにする項目を管理画面で設定できるように +- Enhance: 絵文字ピッカーのカテゴリに「/」を入れることでフォルダ分け表示できるように +- Fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 +- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 +- Fix: コードエディタが正しく表示されない問題を修正 +- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正 +- Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正 +- Fix: 共有機能をサポートしていないブラウザの場合は共有ボタンを非表示にする #11305 +- Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470 +- Fix: 長い名前のチャンネルにおける投稿フォームの表示が崩れる問題を修正 +- Fix: セキュリティ向上のためAiScriptの`Mk:apiExternal`を無効化 +- Fix: ノート中の絵文字をタップして「リアクションする」からリアクションした際にリアクションサウンドが鳴らない不具合を修正 +- Fix: ノート中のリアクションの表示を微調整 #12650 +- Fix: AiScriptの`readline`が不正な値を返すことがある問題を修正 +- Fix: 投票のみ/画像のみの引用RNが、通知欄でただのRNとして判定されるバグを修正 +- Fix: CWをつけて引用RNしても、普通のRNとして扱われてしまうバグを修正しました。 +- Fix: 「画像が1枚のみのメディアリストの高さ」を「デフォルト」以外に設定していると、CWの中などに添付された画像が見られないバグを修正 +- Fix: DeepL TranslationのPro accountトグルスイッチが表示されていなかったのを修正 +- Fix: twitterの埋め込みカード内リンクからリンク先を開けない問題を修正 +- Fix: WebKitブラウザー上でも「デバイスの画面を常にオンにする」機能が効くように +- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正 +- Fix: MFMでルビの中のテキストがnyaizeされない問題を修正 + +### Server +- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように +- Enhance: Meilisearchを有効にした検索で、ユーザーのミュートやブロックを考慮するように +- Enhance: カスタム絵文字のインポート時の動作を改善 +- Enhance: json-schema(OpenAPIの戻り値として使用されるスキーマ定義)を出来る限り最新化 #12311 +- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303 +- Fix: ロールタイムラインが保存されない問題を修正 +- Fix: api.jsonの生成ロジックを改善 #12402 +- Fix: 招待コードが使い回せる問題を修正 +- Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正 +- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正 +- Fix: リストタイムラインにてミュートが機能しないケースがある問題と、チャンネル投稿がストリーミングで流れてきてしまう問題を修正 #10443 +- Fix: 「みつける」のなかにミュートしたユーザが現れてしまう問題を修正 #12383 +- Fix: Social/Local/Home Timelineにてインスタンスミュートが効かない問題 +- Fix: ユーザのノート一覧にてインスタンスミュートが効かない問題 +- Fix: チャンネルのノート一覧にてインスタンスミュートが効かない問題 +- Fix: 「みつける」が年越し時に壊れる問題を修正 +- Fix: アカウントをブロックした際に、自身のユーザーのページでノートが相手に表示される問題を修正 +- Fix: モデレーションログがモデレーターは閲覧できないように修正 +- Fix: ハッシュタグのトレンド除外設定が即時に効果を持つように修正 +- Fix: HTTP Digestヘッダのアルゴリズム部分に大文字の"SHA-256"しか使えない + ## 2023.11.1 +### Note +- 悪意のある第三者がリモートユーザーになりすました任意のアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-3f39-6537-3cgc)をご覧ください。 + ### General - Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました - Enhance: ローカリゼーションの更新 @@ -53,7 +194,7 @@ ### General - Feat: アイコンデコレーション機能 - サーバーで用意された画像をアイコンに重ねることができます - - 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png + - 画像のテンプレートはこちらです: https://misskey-hub.net/brand-assets/ - 最大でも黄色いエリア内にデコレーションを収めることを推奨します。 - 画像は512x512pxを推奨します。 - Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加 @@ -70,7 +211,7 @@ ### Client - Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました - 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください - https://misskey-hub.net/docs/advanced/publish-on-your-website.html + https://misskey-hub.net/docs/for-developers/publish-on-your-website/ - Feat: 通知をグルーピングして表示するオプション(オプトアウト) - Feat: Misskeyの基本的なチュートリアルを実装 - Feat: スワイプしてタイムラインを再読込できるように diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13e0656041..7f6c1f4f82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,6 +117,10 @@ command. - Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). - Vite HMR (just the `vite` command) is available. The behavior may be different from production. - Service Worker is watched by esbuild. +- The front end can be viewed by accessing `http://localhost:5173`. +- The backend listens on the port configured with `port` in .config/default.yml. +If you have not changed it from the default, it will be "http://localhost:3000". +If "port" in .config/default.yml is set to something other than 3000, you need to change the proxy settings in packages/frontend/vite.config.local-dev.ts. ### Dev Container Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. diff --git a/Dockerfile b/Dockerfile index 66018a01c8..440c04e2df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,101 +1,62 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=20.5.1-bullseye +ARG NODE_VERSION=21.4.0-alpine3.18 -# build assets & compile TypeScript - -FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS native-builder - -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - rm -f /etc/apt/apt.conf.d/docker-clean \ - ; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ - && apt-get update \ - && apt-get install -yqq --no-install-recommends \ - build-essential curl ca-certificates - -ARG TARGETARCH - -RUN curl -L https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-$TARGETARCH-static.tar.xz -o /ffmpeg.tar.xz \ - && tar xvf /ffmpeg.tar.xz -C / --strip-components 1 --wildcards 'ffmpeg-*-static/ffmpeg' 'ffmpeg-*-static/ffprobe' +FROM node:${NODE_VERSION} as build RUN corepack enable WORKDIR /sharkey -COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] -COPY --link ["scripts", "./scripts"] -COPY --link ["packages/megalodon/package.json", "./packages/megalodon/"] -COPY --link ["packages/backend/package.json", "./packages/backend/"] -COPY --link ["packages/frontend/package.json", "./packages/frontend/"] -COPY --link ["packages/sw/package.json", "./packages/sw/"] -COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] +RUN apk add git linux-headers build-base +ENV PYTHONUNBUFFERED=1 +RUN apk add --update python3 && ln -sf python3 /usr/bin/python +RUN python3 -m ensurepip +RUN pip3 install --no-cache --upgrade pip setuptools + +COPY . ./ + +RUN git submodule update --init --recursive +RUN pnpm config set fetch-retries 5 RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ - pnpm i --frozen-lockfile --aggregate-output - -COPY --link . ./ - -ARG NODE_ENV=production - -RUN git submodule update --init + pnpm i RUN pnpm build -RUN rm -rf .git/ - -# build native dependencies for target platform - -FROM --platform=$TARGETPLATFORM node:${NODE_VERSION} AS target-builder - -RUN apt-get update \ - && apt-get install -yqq --no-install-recommends \ - build-essential - -RUN corepack enable - -WORKDIR /sharkey - -COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] -COPY --link ["scripts", "./scripts"] -COPY --link ["packages/megalodon/package.json", "./packages/megalodon/"] -COPY --link ["packages/backend/package.json", "./packages/backend/"] - +RUN node scripts/trim-deps.mjs +RUN mv packages/frontend/assets sharkey-assets +RUN rm -r node_modules packages/frontend packages/sw RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ - pnpm i --frozen-lockfile --aggregate-output + pnpm i --prod +RUN rm -rf .git -FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner +FROM node:${NODE_VERSION} -ARG UID="991" -ARG GID="991" - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - tini curl libjemalloc-dev libjemalloc2 \ - && ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \ - && corepack enable \ - && groupadd -g "${GID}" sharkey \ - && useradd -l -u "${UID}" -g "${GID}" -m -d /sharkey sharkey \ - && find / -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ - && find / -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists - -USER sharkey WORKDIR /sharkey -COPY --chown=sharkey:sharkey --from=target-builder /sharkey/node_modules ./node_modules -COPY --chown=sharkey:sharkey --from=target-builder /sharkey/packages/megalodon/node_modules ./packages/megalodon/node_modules -COPY --chown=sharkey:sharkey --from=target-builder /sharkey/packages/backend/node_modules ./packages/backend/node_modules -COPY --chown=sharkey:sharkey --from=native-builder /ffmpeg /usr/local/bin/ -COPY --chown=sharkey:sharkey --from=native-builder /ffprobe /usr/local/bin/ -COPY --chown=sharkey:sharkey --from=native-builder /sharkey/built ./built -COPY --chown=sharkey:sharkey --from=native-builder /sharkey/packages/megalodon/lib ./packages/megalodon/lib -COPY --chown=sharkey:sharkey --from=native-builder /sharkey/packages/backend/built ./packages/backend/built -COPY --chown=sharkey:sharkey --from=native-builder /sharkey/fluent-emojis /sharkey/fluent-emojis -COPY --chown=sharkey:sharkey . ./ +RUN apk add ffmpeg tini + +COPY --from=build /sharkey/built ./built +COPY --from=build /sharkey/node_modules ./node_modules +COPY --from=build /sharkey/packages/backend/built ./packages/backend/built +COPY --from=build /sharkey/packages/backend/node_modules ./packages/backend/node_modules +COPY --from=build /sharkey/packages/megalodon/lib ./packages/megalodon/lib +COPY --from=build /sharkey/packages/megalodon/node_modules ./packages/megalodon/node_modules +COPY --from=build /sharkey/packages/misskey-js/built ./packages/misskey-js/built +COPY --from=build /sharkey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules +COPY --from=build /sharkey/fluent-emojis ./fluent-emojis +COPY --from=build /sharkey/sharkey-assets ./packages/frontend/assets + +COPY package.json ./package.json +COPY pnpm-workspace.yaml ./pnpm-workspace.yaml +COPY packages/backend/package.json ./packages/backend/package.json +COPY packages/backend/check_connect.js ./packages/backend/check_connect.js +COPY packages/backend/ormconfig.js ./packages/backend/ormconfig.js +COPY packages/backend/migration ./packages/backend/migration +COPY packages/backend/assets ./packages/backend/assets +COPY packages/megalodon/package.json ./packages/megalodon/package.json +COPY packages/misskey-js/package.json ./packages/misskey-js/package.json -ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so ENV NODE_ENV=production -VOLUME "/sharkey/files" -HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/sharkey/healthcheck.sh"] -ENTRYPOINT ["/usr/bin/tini", "--"] +RUN corepack enable +ENTRYPOINT ["/sbin/tini", "--"] CMD ["pnpm", "run", "migrateandstart"] diff --git a/README.md b/README.md index 8b9da7e712..d3ba23c4e1 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ find an instance + + create an instance + become a contributor @@ -49,3 +52,7 @@ With Sharkey, you can enable sign-ups, subject to manual moderator approval and
+ +## Documentation + +Sharkey Documentation can be found at [Sharkey Documentation](https://docs.joinsharkey.org/docs/install/fresh/) diff --git a/chart/files/default.yml b/chart/files/default.yml index 87b2f677eb..9c81964736 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -77,17 +77,17 @@ dbReplications: false # You can configure any number of replicas here #dbSlaves: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -167,7 +167,7 @@ id: "aidx" # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 16 +# inboxJobPerSec: 32 # Job attempts # deliverJobMaxAttempts: 12 @@ -194,6 +194,8 @@ id: "aidx" # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true +# check that inbound ActivityPub GET requests are signed ("authorized fetch") +checkActivityPubGetSignature: false #allowedPrivateNetworks: [ # '127.0.0.1/32' diff --git a/chart/templates/Deployment.yml b/chart/templates/Deployment.yml index d5dd14f59e..3c73837801 100644 --- a/chart/templates/Deployment.yml +++ b/chart/templates/Deployment.yml @@ -27,7 +27,7 @@ spec: ports: - containerPort: 3000 - name: postgres - image: postgres:14-alpine + image: postgres:15-alpine env: - name: POSTGRES_USER value: "example-misskey-user" @@ -38,7 +38,7 @@ spec: ports: - containerPort: 5432 - name: redis - image: redis:alpine + image: redis:7-alpine ports: - containerPort: 6379 volumes: diff --git a/docker-compose.local-db.yml b/docker-compose.local-db.yml new file mode 100644 index 0000000000..16ba4b49e1 --- /dev/null +++ b/docker-compose.local-db.yml @@ -0,0 +1,42 @@ +version: "3" + +# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します + +services: + redis: + restart: always + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - ./redis:/data + healthcheck: + test: "redis-cli ping" + interval: 5s + retries: 20 + + db: + restart: always + image: postgres:15-alpine + ports: + - "5432:5432" + env_file: + - .config/docker.env + volumes: + - ./db:/var/lib/postgresql/data + healthcheck: + test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" + interval: 5s + retries: 20 + +# meilisearch: +# restart: always +# image: getmeili/meilisearch:v1.3.4 +# environment: +# - MEILI_NO_ANALYTICS=true +# - MEILI_ENV=production +# env_file: +# - .config/meilisearch.env +# volumes: +# - ./meili_data:/meili_data + diff --git a/docker-compose.yml.example b/docker-compose_example.yml similarity index 92% rename from docker-compose.yml.example rename to docker-compose_example.yml index 1e94c35d47..5a8560bb42 100644 --- a/docker-compose.yml.example +++ b/docker-compose_example.yml @@ -2,6 +2,7 @@ version: "3" services: web: +# replace image below with git.joinsharkey.org/sharkey/sharkey:stable on next release # image: ghcr.io/transfem-org/sharkey:stable build: . restart: always @@ -54,8 +55,6 @@ services: # environment: # - MEILI_NO_ANALYTICS=true # - MEILI_ENV=production -# env_file: -# - .config/meilisearch.env # networks: # - shonk # volumes: diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index d62990b7b7..0a7d86cc89 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -120,7 +120,6 @@ sensitive: "محتوى حساس" add: "إضافة" reaction: "التفاعلات" reactions: "التفاعلات" -reactionSetting: "التفاعلات المراد عرضها في منتقي التفاعلات." reactionSettingDescription2: "اسحب لترتيب ، انقر للحذف ، استخدم \"+\" للإضافة." rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات" attachCancel: "أزل المرفق" @@ -418,7 +417,6 @@ share: "شارِك" notFound: "غير موجود" notFoundDescription: "تعذر العثور على صفحة يقود إليها هذا الرابط." uploadFolder: "المجلد الافتراضي للرفع" -cacheClear: "مسح ذاكرة التخزين المؤقت" markAsReadAllNotifications: "وضع جميع الإشعارات كأنها مقروءة" markAsReadAllUnreadNotes: "علّم جميع الملاحظات كمقروءة" markAsReadAllTalkMessages: "علّم جميع الرسائل كمقروءة" @@ -818,8 +816,6 @@ makeReactionsPublicDescription: "هذا سيجعل قائمة تفاعلاتك classic: "تقليدي" muteThread: "اكتم النقاش" unmuteThread: "ارفع الكتم عن النقاش" -ffVisibility: "مرئية المتابِعين/المتابَعين" -ffVisibilityDescription: "يسمح لك بتحديد من يمكنهم رؤية متابِعيك ومتابَعيك." continueThread: "اعرض بقية النقاش" deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد المتابعة؟" incorrectPassword: "كلمة السر خاطئة." @@ -948,9 +944,12 @@ rolesAssignedToMe: "الأدوار المسندة إلي" resetPasswordConfirm: "هل تريد إعادة تعيين كلمة السر؟" license: "الرخصة" unfavoriteConfirm: "أتريد إزالتها من المفضلة؟" +reactionsDisplaySize: "حجم التفاعلات" +limitWidthOfReaction: "تصغير حجم التفاعلات" noteIdOrUrl: "معرف الملاحظة أو رابطها" video: "فيديو" videos: "فيديوهات" +dataSaver: "موفر البيانات" accountMigration: "ترحيل الحساب" accountMoved: "نقل هذا المستخدم حسابه:" accountMovedShort: "رُحل هذا الحساب." @@ -958,6 +957,7 @@ operationForbidden: "عملية ممنوعة" forceShowAds: "أظهر الإعلانات التجارية دائما" reactionsList: "التفاعلات" renotesList: "إعادات النشر" +notificationDisplay: "إشعارات" leftTop: "أعلى اليسار" rightTop: "أعلى اليمين" leftBottom: "أسفل اليسار" @@ -980,6 +980,7 @@ thisChannelArchived: "أُرشفت هذه القناة." displayOfNote: "عرض الملاحظة" initialAccountSetting: "إعداد الملف الشخصي" youFollowing: "متابَع" +preventAiLearning: "منع استخدام البيانات في تعليم الآلة" options: "خيارات" specifyUser: "مستخدم محدد" failedToPreviewUrl: "تتعذر المعاينة" @@ -993,7 +994,16 @@ later: "لاحقاً" goToMisskey: "لميسكي" additionalEmojiDictionary: "قواميس إيموجي إضافية" installed: "مُثبت" +enableServerMachineStats: "نشر إحصائيات عتاد الخادم" +turnOffToImprovePerformance: "تفعيله قد يزيد الأداء." +createInviteCode: "ولِّد دعوة" +inviteCodeCreated: "ولِّدت دعوة" +inviteLimitExceeded: "وصلتَ لحد عدد الدعوات المسموح لك توليدها." +createLimitRemaining: "حد عدد الدعوات: {limit} دعوة" expirationDate: "تاريخ انتهاء الصلاحية" +noExpirationDate: "لا نهاية لصلاحيتها" +inviteCodeUsedAt: "اُستخدم رمز الدعوة في" +registeredUserUsingInviteCode: "اِستخدم رمز الدعوة" unused: "غير مستعمَل" expired: "منتهية صلاحيته" icon: "الصورة الرمزية" @@ -1550,3 +1560,4 @@ _webhookSettings: _moderationLogTypes: suspend: "علِق" resetPassword: "أعد تعيين كلمتك السرية" + createInvitation: "ولِّد دعوة" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 31f2b948ed..77ba3f0306 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -2,6 +2,7 @@ _lang_: "বাংলা" headlineMisskey: "নোট ব্যাবহার করে সংযুক্ত নেটওয়ার্ক" introMisskey: "স্বাগতম! মিসকি একটি ওপেন সোর্স, ডিসেন্ট্রালাইজড মাইক্রোব্লগিং পরিষেবা। \n\"নোট\" তৈরির মাধ্যমে যা ঘটছে তা সবার সাথে শেয়ার করুন 📡\n\"রিঅ্যাকশন\" গুলির মাধ্যমে যেকোনো নোট সম্পর্কে আপনার অনুভূতি ব্যাক্ত করতে পারেন 👍\nএকটি নতুন দুনিয়া ঘুরে দেখুন 🚀\n" +poweredByMisskeyDescription: "{name} হল ওপেন সোর্স প্ল্যাটফর্ম Misskey-এর সার্ভারগুলির একটি৷" monthAndDay: "{day}/{month}" search: "খুঁজুন" notifications: "বিজ্ঞপ্তি" @@ -12,12 +13,14 @@ fetchingAsApObject: "ফেডিভার্স থেকে খবর আন ok: "ঠিক" gotIt: "বুঝেছি" cancel: "বাতিল" +noThankYou: "না, ধন্যবাদ" enterUsername: "ইউজারনেম লিখুন" renotedBy: "{user} রিনোট করেছেন" noNotes: "কোন নোট নেই" noNotifications: "কোনো বিজ্ঞপ্তি নেই" instance: "ইন্সট্যান্স" settings: "সেটিংস" +notificationSettings: "বিজ্ঞপ্তির সেটিংস" basicSettings: "সাধারণ সেটিংস" otherSettings: "অন্যান্য সেটিংস" openInWindow: "নতুন উইন্ডোতে খুলা" @@ -42,12 +45,20 @@ pin: "পিন করা" unpin: "পিন সরান" copyContent: "বিষয়বস্তু কপি করুন" copyLink: "লিঙ্ক কপি করুন" +copyLinkRenote: "রিনোট লিঙ্ক কপি করুন" delete: "মুছুন" deleteAndEdit: "মুছুন এবং সম্পাদনা করুন" deleteAndEditConfirm: "আপনি কি এই নোটটি মুছে এটি সম্পাদনা করার বিষয়ে নিশ্চিত? আপনি এটির সমস্ত রিঅ্যাকশন, রিনোট এবং জবাব হারাবেন।" addToList: "লিস্ট এ যোগ করুন" +addToAntenna: "অ্যান্টেনা এ যোগ করুন" sendMessage: "একটি বার্তা পাঠান" +copyRSS: "RSS কপি করুন" copyUsername: "ব্যবহারকারীর নাম কপি করুন" +copyUserId: "ব্যবহারকারীর ID কপি করুন" +copyNoteId: "নোটের ID কপি করুন" +copyFileId: "ফাইল ID কপি করুন" +copyFolderId: "ফোল্ডার ID কপি করুন" +copyProfileUrl: "প্রোফাইল URL কপি করুন" searchUser: "ব্যবহারকারী খুঁজুন..." reply: "জবাব" loadMore: "আরও দেখুন" @@ -100,6 +111,8 @@ renoted: "রিনোট করা হয়েছে" cantRenote: "এই নোটটি রিনোট করা যাবে না।" cantReRenote: "রিনোটকে রিনোট করা যাবে না।" quote: "উদ্ধৃতি" +inChannelRenote: "চ্যানেলে রিনোট" +inChannelQuote: "চ্যানেলে উদ্ধৃতি" pinnedNote: "পিন করা নোট" pinned: "পিন করা" you: "আপনি" @@ -108,7 +121,10 @@ sensitive: "সংবেদনশীল বিষয়বস্তু" add: "যুক্ত করুন" reaction: "প্রতিক্রিয়া" reactions: "প্রতিক্রিয়া" -reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে" +emojiPicker: "ইমোজি পিকার" +pinnedEmojisForReactionSettingDescription: "রিঅ্যাকশন দেয়ার সময় আপনি ইমোজিটিকে পিন করা এবং প্রদর্শিত হওয়ার জন্য সেট করতে পারেন।" +pinnedEmojisSettingDescription: "ইমোজি ইনপুট দেয়ার সময় আপনি ইমোজিটিকে পিন করা এবং প্রদর্শিত হওয়ার জন্য সেট করতে পারেন।" +emojiPickerDisplay: "পিকার ডিসপ্লে" reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।" rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন" attachCancel: "অ্যাটাচমেন্ট সরান " @@ -393,7 +409,6 @@ share: "শেয়ার" notFound: "পাওয়া যায়নি" notFoundDescription: "এই URL-এর সাথে সম্পর্কিত কোনো পৃষ্ঠা নেই।" uploadFolder: "আপলোডের জন্য ডিফল্ট ফোল্ডার" -cacheClear: "ক্যাশ পরিষ্কার করুন" markAsReadAllNotifications: "সমস্ত বিজ্ঞপ্তিগুলি পঠিত হিসাবে চিহ্নিত করুন" markAsReadAllUnreadNotes: "সমস্ত নোটগুলি পঠিত হিসাবে চিহ্নিত করুন" markAsReadAllTalkMessages: "সমস্ত মেসেজ পঠিত হিসাবে চিহ্নিত করুন" @@ -795,8 +810,6 @@ makeReactionsPublicDescription: "আপনার পূর্ববর্তী classic: "ক্লাসিক" muteThread: "থ্রেড মিউট করুন" unmuteThread: "থ্রেড আনমিউট করুন" -ffVisibility: "অনুসরণ/অনুসরণকারীদের দৃশ্যমান্যতা" -ffVisibilityDescription: "আপনি কাকে অনুসরণ করেন এবং কে আপনাকে অনুসরণ করে, সেটা কারা দেখতে পাবে তা নির্ধারণ করে।" continueThread: "আরো থ্রেড দেখুন" deleteAccountConfirm: "আপনার অ্যাকাউন্ট মুছে ফেলা হবে। ঠিক আছে?" incorrectPassword: "আপনার দেওয়া পাসওয়ার্ডটি ভুল।" @@ -1038,6 +1051,7 @@ _2fa: step3: "অ্যাপে প্রদর্শিত টোকেনটি লিখুন এবং আপনার কাজ শেষ।" step4: "আপনাকে এখন থেকে লগ ইন করার সময়, এইভাবে টোকেন লিখতে হবে।" securityKeyInfo: "আপনি একটি হার্ডওয়্যার সিকিউরিটি কী ব্যবহার করে লগ ইন করতে পারেন যা FIDO2 বা ডিভাইসের ফিঙ্গারপ্রিন্ট সেন্সর বা পিন সমর্থন করে৷" + renewTOTPCancel: "না, ধন্যবাদ" _permissions: "read:account": "অ্যাকাউন্টের তথ্য দেখুন" "write:account": "অ্যাকাউন্টের তথ্য সম্পাদন করুন" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 6fd72d19b7..ba1e44256e 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -121,7 +121,12 @@ sensitive: "NSFW" add: "Afegir" reaction: "Reaccions" reactions: "Reaccions" -reactionSetting: "Reaccions a mostrar al selector de reaccions" +emojiPicker: "Selecció d'emojis" +pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb el qual reaccionar" +pinnedEmojisSettingDescription: "Selecciona l'emoji amb el qual reaccionar" +emojiPickerDisplay: "Visualitza el selector d'emojis" +overwriteFromPinnedEmojisForReaction: "Reemplaça els emojis de la reacció" +overwriteFromPinnedEmojis: "Sobreescriu des dels emojis fixats" reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir." rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes" attachCancel: "Eliminar el fitxer adjunt" @@ -157,6 +162,9 @@ addEmoji: "Afegeix un emoji" settingGuide: "Configuració recomanada" cacheRemoteFiles: "Emmagatzemar fitxers remots" cacheRemoteFilesDescription: "Quan aquesta opció està desactivada, els fitxers remots es carreguen directament des del servidor remot. Si desactiveu això, es reduirà l'ús d'emmagatzematge, però augmentarà el trànsit, ja que no es generaran miniatures." +youCanCleanRemoteFilesCache: "Pots netejar la memòria cau fent clic al botó de la paperera🗑️ a l'administrador d'arxius." +cacheRemoteSensitiveFiles: "Posar a la memòria cau arxius remots sensibles" +cacheRemoteSensitiveFilesDescription: "Quan aquesta opció és desactiva, els arxius remots sensibles es carregant directament del servidor d'origen sense que es guardin a la memòria cau." flagAsBot: "Marca aquest compte com a bot" flagAsBotDescription: "Marca aquest compte com a bot" flagAsCat: "Marca aquest compte com a gat" @@ -165,6 +173,7 @@ flagShowTimelineReplies: "Mostra les respostes a la línia de temps" flagShowTimelineRepliesDescription: "Mostra les respostes a la línia de temps" autoAcceptFollowed: "Aprova automàticament les sol·licituds de seguiment dels usuaris que segueixes" addAccount: "Afegeix un compte" +reloadAccountsList: "Recarregar la llista de contactes" loginFailed: "S'ha produït un error al accedir." showOnRemote: "Navega més en el perfil original" general: "General" @@ -191,6 +200,7 @@ perHour: "Per hora" perDay: "Per dia" stopActivityDelivery: "Deixa d'enviar activitats" blockThisInstance: "Deixa d'enviar activitats" +silenceThisInstance: "Silencia aquesta instància " operations: "Accions" software: "Programari" version: "Versió" @@ -209,6 +219,9 @@ clearQueueConfirmText: "Les notes no lliurades que quedin a la cua no es federar clearCachedFiles: "Esborra la memòria cau" clearCachedFilesConfirm: "Segur que voleu eliminar tots els fitxers de la memòria cau?" blockedInstances: "Instàncies bloquejades" +blockedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols bloquejar separades per un salt de pàgina. Les instàncies llistades no podran comunicar-se amb aquesta instància." +silencedInstances: "Instàncies silenciades" +silencedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols silenciar. Tots els comptes de les instàncies llistades s'establiran com silenciades i només podran fer sol·licitacions de seguiment, i no podran mencionar als comptes locals si no els segueixen. Això no afectarà les instàncies bloquejades." muteAndBlock: "Silencia i bloca" mutedUsers: "Usuaris silenciats" blockedUsers: "Usuaris bloquejats" @@ -223,9 +236,12 @@ preview: "Vista prèvia" default: "Per defecte" defaultValueIs: "Per defecte: {value}" noCustomEmojis: "Cap emoji personalitzat" +noJobs: "No hi ha feines" federating: "Federant" blocked: "Bloquejat" suspended: "Suspés" +all: "tot" +subscribing: "Subscrit a" publishing: "S'està publicant" notResponding: "Sense resposta" instanceFollowing: "Seguits del servidor" @@ -250,11 +266,31 @@ removed: "Eliminat" removeAreYouSure: "Segur que voleu retirar «{x}»?" deleteAreYouSure: "Segur que voleu retirar «{x}»?" resetAreYouSure: "Segur que voleu restablir-ho?" +areYouSure: "Està segur?" saved: "S'ha desat" messaging: "Xat" upload: "Puja" +keepOriginalUploading: "Guarda la imatge original" +keepOriginalUploadingDescription: "Guarda la imatge pujada com hi és. Si està apagat, una versió per a la visualització a la xarxa serà generada quan sigui pujada." +fromDrive: "Des de la unitat" +fromUrl: "Des d'un enllaç" +uploadFromUrl: "Carrega des d'un enllaç" +uploadFromUrlDescription: "Enllaç del fitxer que vols carregar" +uploadFromUrlRequested: "Càrrega sol·licitada" +uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot prendre un temps" +explore: "Explora" +messageRead: "Vist" +noMoreHistory: "No hi resta més per veure" +startMessaging: "Començar a xatejar" +nUsersRead: "Vist per {n}" +agreeTo: "Accepto que {0}" +agree: "Hi estic d'acord" +agreeBelow: "Hi estic d'acord amb el següent" +basicNotesBeforeCreateAccount: "Notes importants" +termsOfService: "Condicions d'ús" start: "Comença" home: "Inici" +remoteUserCaution: "Ja que aquest usuari resideix a una instància remota, la informació mostrada es podria trobar incompleta." activity: "Activitat" images: "Imatges" image: "Imatges" @@ -270,16 +306,34 @@ dark: "Fosc" lightThemes: "Temes clars" darkThemes: "Temes foscos" syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu" +drive: "Unitat" +fileName: "Nom del Fitxer" +selectFile: "Selecciona fitxers" +selectFiles: "Selecciona fitxers" +selectFolder: "Selecció de carpeta" +selectFolders: "Selecció de carpeta" renameFile: "Canvia el nom del fitxer" folderName: "Nom de la carpeta" createFolder: "Crea una carpeta" renameFolder: "Canvia el nom de la carpeta" deleteFolder: "Elimina la carpeta" +folder: "Carpeta " addFile: "Afegeix un fitxer" +emptyDrive: "La teva unitat és buida" emptyFolder: "La carpeta està buida" unableToDelete: "No es pot eliminar" +inputNewFileName: "Introduïu el nom de fitxer nou" +inputNewDescription: "Inserta una nova llegenda" +inputNewFolderName: "Introduïu el nom de la carpeta nova" +circularReferenceFolder: "La carpeta destinatària és una subcarpeta de la carpeta a la qual la desitges moure" +hasChildFilesOrFolders: "No és possible esborrar aquesta carpeta ja que no és buida" copyUrl: "Copia l'URL" rename: "Canvia el nom" +avatar: "Icona" +banner: "Bàner" +displayOfSensitiveMedia: "Visualització de contingut sensible" +whenServerDisconnected: "Quan es perdi la connexió al servidor" +disconnectedFromServer: "Desconnectat pel servidor" reload: "Actualitza" doNothing: "Ignora" accept: "Accepta" @@ -349,33 +403,132 @@ notFound: "No s'ha trobat" markAsReadAllUnreadNotes: "Marca-ho tot com a llegit" help: "Ajuda" invites: "Convida" +title: "Títol" +text: "Text" +enable: "Habilita" next: "Següent" +retype: "Torneu a introduir-la" noteOf: "Publicació de: {user}" +quoteAttached: "Frase adjunta" +quoteQuestion: "Vols annexar-la com a cita?" +noMessagesYet: "Encara no hi ha missatges" +newMessageExists: "Has rebut un nou missatge" +onlyOneFileCanBeAttached: "Només pots adjuntar un fitxer a un missatge" +signinRequired: "Si us plau, Registra't o inicia la sessió abans de continuar" invitations: "Convida" +invitationCode: "Codi d'invitació" +checking: "Comprovació en curs..." +available: "Disponible" +unavailable: "No és disponible" +usernameInvalidFormat: "Pots fer servir lletres (majúscules i minúscules), números i barres baixes (\"_\")" +tooShort: "Massa curt" +tooLong: "Massa llarg" +weakPassword: "Contrasenya insegura" +normalPassword: "Bona contrasenya" +strongPassword: "Contrasenya segura" +passwordMatched: "Correcte!" +passwordNotMatched: "No coincideix" +signinWith: "Inicia sessió amb amb {x}" +signinFailed: "Autenticació sense èxit. Intenta-ho un altre cop utilitzant la contrasenya i el nom correctes." +or: "O" +language: "Idioma" +uiLanguage: "Idioma de l'interfície" +aboutX: "Respecte a {x}" +emojiStyle: "Estil d'emoji" +native: "Nadiu" +disableDrawer: "No mostrar els menús en calaixos" +showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor" +noHistory: "No hi ha un registre previ" +signinHistory: "Historial d'autenticacions" +enableAdvancedMfm: "Habilitar l'MFM avançat" +enableAnimatedMfm: "Habilitar l'MFM amb moviment" +doing: "Processant..." +category: "Categoria" tags: "Etiquetes" docSource: "Font del document" createAccount: "Crea un compte" existingAccount: "Compte existent" regenerate: "Regenera" fontSize: "Mida del text" +mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única imatge" +limitTo: "Limita a {x}" noFollowRequests: "No tens sol·licituds de seguiment" +openImageInNewTab: "Obre imatges a una nova pestanya" dashboard: "Panell de control" local: "Local" remote: "Remot" total: "Total" +weekOverWeekChanges: "Canvis l'última setmana" +dayOverDayChanges: "Canvis ahir" appearance: "Aparença" clientSettings: "Configuració del client" accountSettings: "Configuració del compte" +promotion: "Promocionat" +promote: "Promoure" +numberOfDays: "Nombre de dies" hideThisNote: "Amaga la publicació" showFeaturedNotesInTimeline: "Mostra publicacions destacades en la línia de temps" +objectStorage: "Emmagatzematge d'objectes\n" +useObjectStorage: "Utilitzar l'emmagatzematge d'objectes" +objectStorageBaseUrl: "Base d'enllaç" +objectStorageBaseUrlDesc: "Prefix d'enllaç utilitzat per a fer referencia als fitxers. Especifica l'enllaç del teu CDN o Proxy si n'estàs utilitzant qualsevol, en cas contrari, especifica l'enllaç al que es pot accedir públicament segons la guia de servei que vosté utilitza.\nPer l'ús d'S3 utilitza 'https://.s3.amazonaws.com' I per a GCS o serveis equivalents utilitza 'https://storage.googleapis.com/'." newNoteRecived: "Hi ha publicacions noves" installedDate: "Data d'instal·lació" state: "Estat" sort: "Ordena" ascendingOrder: "Ascendent" descendingOrder: "Descendent" +removeAllFollowing: "Deixar de seguir tots els usuaris seguits" +removeAllFollowingDescription: "El fet d'executar això, et farà deixar de seguir a tots els usuaris de {host}. Si us plau, executa això si l'amfitrió, per exemple, ja no existeix." +userSuspended: "Aquest usuari ha sigut suspès" +userSilenced: "Aquest usuari està sent silenciat" +yourAccountSuspendedTitle: "Aquest compte és suspès" +yourAccountSuspendedDescription: "Aquest compte ha sigut suspès a causa de la violació de les condicions d'ús o similars. Contacta l'administrador si en vol saber més. Si us plau, no en faci un altre compte." +tokenRevoked: "Codi de seguretat no vàlid" +tokenRevokedDescription: "La petició més recent ha estat denegada perquè contenia un codi de seguretat no vàlid. Actualitza la pàgina i torna-ho a provar." +accountDeleted: "Compte eliminat amb èxit" +accountDeletedDescription: "Aquest compte ha sigut eliminat" +menu: "Menú" +divider: "Divisor" +addItem: "Afegir element" +rearrange: "Torna a ordenar" +relays: "Relés" +addRelay: "Afegeix relés" +inboxUrl: "Enllaç de la safata d'entrada" +addedRelays: "Relés afegits" +serviceworkerInfo: "És obligatòria l'activació per a obtenir notificacions push" deletedNote: "Publicacions eliminades" invisibleNote: "Publicacions amagades" +enableInfiniteScroll: "Carrega més automàticament\n" +visibility: "Visibilitat" +poll: "Enquesta" +useCw: "Amaga el contingut" +enablePlayer: "Obre el reproductor de vídeo" +disablePlayer: "Tanca el reproductor de vídeo" +expandTweet: "Expandir post" +themeEditor: "Editor de temes" +description: "Descripció" +describeFile: "Afegir subtitulació" +enterFileDescription: "Afegeix un títol" +author: "Autor" +leaveConfirm: "Hi ha canvis sense guardar. Els vols descartar?" +manage: "Administració" +plugins: "Extensions" +preferencesBackups: "Configuracions de les Còpies de seguretat" +deck: "Escriptori" +undeck: "Tanca l'escriptori" +useBlurEffectForModal: "Utilitzar l'efecte de difuminació a modals" +useFullReactionPicker: "Utilitza el cercador de reaccions d'escala sencera" +width: "Amplada" +height: "Alçària" +large: "Gran" +medium: "Mitjà" +small: "Petit" +generateAccessToken: "Genera codi d'accés" +permission: "Permisos" +enableAll: "Habilita tot" +disableAll: "Deshabilita tot" +tokenRequested: "Donar accés al compte" smtpHost: "Amfitrió" smtpUser: "Nom d'usuari" smtpPass: "Contrasenya" @@ -385,12 +538,17 @@ clearCache: "Esborra la memòria cau" showingPastTimeline: "Estàs veient una línia de temps antiga" info: "Informació" user: "Usuaris" +administration: "Administració" +middle: "Mitjà" global: "Global" searchByGoogle: "Cercar" file: "Fitxers" +icon: "Icona" replies: "Respostes" renotes: "Impulsa" _role: + _priority: + middle: "Mitjà" _options: antennaMax: "Nombre màxim d'antenes" _email: @@ -399,9 +557,11 @@ _email: _instanceMute: instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat." _theme: + description: "Descripció" keys: mention: "Menció" renote: "Renotar" + divider: "Divisor" _sfx: note: "Notes" notification: "Notificacions" @@ -443,6 +603,8 @@ _timelines: local: "Local" social: "Social" global: "Global" +_play: + summary: "Descripció" _pages: contents: "Contingut" blocks: diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 38c52372c3..afe613677f 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -120,7 +120,6 @@ sensitive: "NSFW" add: "Přidat" reaction: "Reakce" reactions: "Reakce" -reactionSetting: "Reakce zobrazené ve výběru reakcí" reactionSettingDescription2: "Přetažením změníte pořadí, kliknutím smažete, zmáčkněte \"+\" k přidání" rememberNoteVisibility: "Zapamatovat nastavení zobrazení poznámky" attachCancel: "Odstranit přílohu" @@ -428,7 +427,6 @@ share: "Sdílet" notFound: "Nenalezeno" notFoundDescription: "Nebyla nalezená žádná stránka korespondující se zadanou URL." uploadFolder: "Výchozí lokace pro upload" -cacheClear: "Vymazat cache" markAsReadAllNotifications: "Označit všechna oznámení za přečtená" markAsReadAllUnreadNotes: "Označit všechny příspěvky za přečtené" markAsReadAllTalkMessages: "Označit všechny zprávy za přečtené" @@ -856,8 +854,6 @@ makeReactionsPublicDescription: "Tohle zviditelný seznam vašich předchozích classic: "Klasický" muteThread: "Ztlumit vlákno" unmuteThread: "Zrušit ztlumení vlákna" -ffVisibility: "Viditelnost Sledovaných/Sledujících" -ffVisibilityDescription: "Umožní vám nastavit kdo uvidí koho sledujete a kdo vás sleduje." continueThread: "Zobrazit pokračování vlákna" deleteAccountConfirm: "Tohle nenávratně smaže váš účet, chcete pokračovat?" incorrectPassword: "Nesprávné heslo." diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 4747ac86b6..83b254b2d5 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -121,7 +121,6 @@ sensitive: "Sensibel" add: "Hinzufügen" reaction: "Reaktionen" reactions: "Reaktionen" -reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen" reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen" rememberNoteVisibility: "Notizsichtbarkeit merken" attachCancel: "Anhang entfernen" @@ -311,6 +310,7 @@ folderName: "Ordnername" createFolder: "Ordner erstellen" renameFolder: "Ordner umbenennen" deleteFolder: "Ordner löschen" +folder: "Ordner" addFile: "Datei hinzufügen" emptyDrive: "Deine Drive ist leer" emptyFolder: "Dieser Ordner ist leer" @@ -437,7 +437,6 @@ share: "Teilen" notFound: "Nicht gefunden" notFoundDescription: "Es konnte keine Seite unter dieser URL gefunden werden." uploadFolder: "Standardordner für Uploads" -cacheClear: "Cache leeren" markAsReadAllNotifications: "Alle Benachrichtigungen als gelesen markieren" markAsReadAllUnreadNotes: "Alle Notizen als gelesen markieren" markAsReadAllTalkMessages: "Alle Chats als gelesen markieren" @@ -544,6 +543,8 @@ showInPage: "In einer Seite anzeigen" popout: "Pop-Up" volume: "Lautstärke" masterVolume: "Gesamtlautstärke" +notUseSound: "Gebe kein Ton aus" +useSoundOnlyWhenActive: "Gebe nur Ton aus, wenn Misskey aktiv ist" details: "Details" chooseEmoji: "Emoji auswählen" unableToProcess: "Der Vorgang konnte nicht abgeschlossen werden" @@ -564,6 +565,10 @@ output: "Ausgabe" script: "Skript" disablePagesScript: "AiScript auf Seiten deaktivieren" updateRemoteUser: "Benutzerinformationen aktualisieren" +unsetUserAvatar: "Entferne Profilbild" +unsetUserAvatarConfirm: "Möchtest du dein Profilbild entfernen?" +unsetUserBanner: "Entferne Profilbanner" +unsetUserBannerConfirm: "Möchtest du dein Profilbanner entfernen?" deleteAllFiles: "Alle Dateien löschen" deleteAllFilesConfirm: "Möchtest du wirklich alle Dateien löschen?" removeAllFollowing: "Allen gefolgten Benutzern entfolgen" @@ -868,8 +873,6 @@ makeReactionsPublicDescription: "Jeder wird die Liste deiner gesendeten Reaktion classic: "Classic" muteThread: "Thread stummschalten" unmuteThread: "Threadstummschaltung aufheben" -ffVisibility: "Sichtbarkeit von Gefolgten/Followern" -ffVisibilityDescription: "Konfiguriere wer sehen kann, wem du folgst sowie wer dir folgt." continueThread: "Weiteren Threadverlauf anzeigen" deleteAccountConfirm: "Dein Benutzerkonto wird unwiderruflich gelöscht. Trotzdem fortfahren?" incorrectPassword: "Falsches Passwort." @@ -1020,6 +1023,8 @@ resetPasswordConfirm: "Wirklich Passwort zurücksetzen?" sensitiveWords: "Sensible Wörter" sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden." sensitiveWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden." +hiddenTags: "Ausgeblendete Hashtags" +hiddenTagsDescription: "Die hier eingestellten Tags werden nicht mehr in den Trends angezeigt. Mit der Umschalttaste können mehrere ausgewählt werden." notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar." license: "Lizenz" unfavoriteConfirm: "Wirklich aus Favoriten entfernen?" @@ -1032,6 +1037,7 @@ enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen" enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen" showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen" reactionsDisplaySize: "Reaktionsanzeigegröße" +limitWidthOfReaction: "Begrenze die Breite der Reaktion und zeige sie verkleinert an" noteIdOrUrl: "Notiz-ID oder URL" video: "Video" videos: "Videos" @@ -1155,7 +1161,10 @@ refreshing: "Wird aktualisiert..." pullDownToRefresh: "Zum Aktualisieren ziehen" disableStreamingTimeline: "Echtzeitaktualisierung der Chronik deaktivieren" useGroupedNotifications: "Benachrichtigungen gruppieren" +signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen. Der Link könnte abgelaufen sein." cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden." +doReaction: "Reagieren" +code: "Code" _announcement: 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." @@ -1165,6 +1174,9 @@ _announcement: tooManyActiveAnnouncementDescription: "Zu viele aktive Ankündigungen können die Benutzerfreundlichkeit verschlechtern. Es wird empfohlen, veraltete Ankündigungen zu archivieren." readConfirmTitle: "Als gelesen markieren?" readConfirmText: "Dies markiert den Inhalt von \"{title}\" als gelesen." + dialogAnnouncementUxWarn: "Bei der Verwendung von mehr als zwei Meldungen im Dialog-Format wird um Vorsicht geboten, da dies negative Auswirkungen auf die UX haben kann." + silence: "Keine Benachrichtigung" + silenceDescription: "Wenn aktiviert, gibt diese Meldung keine Nachricht aus und muss nicht als \"gelesen\" markiert werden." _initialAccountSetting: accountCreated: "Dein Konto wurde erfolgreich erstellt!" letsStartAccountSetup: "Lass uns nun dein Konto einrichten." @@ -1177,8 +1189,20 @@ _initialAccountSetting: pushNotificationDescription: "Durch die Aktivierung von Push-Benachrichtigungen kannst du von {name} Benachrichtigungen direkt auf dein Gerät erhalten." initialAccountSettingCompleted: "Kontoeinrichtung abgeschlossen!" haveFun: "Viel Spaß mit {name}!" + youCanContinueTutorial: "Du kannst mit dem Tutorial von {name}(Misskey) fortfahren, oder auch abbrechen und gleich anfangen Misskey zu benutzen." + startTutorial: "Fange mit dem Tutorial an" skipAreYouSure: "Die Kontoeinrichtung wirklich überspringen?" laterAreYouSure: "Die Kontoeinrichtung wirklich später erledigen?" +_initialTutorial: + launchTutorial: "Tutorial ansehen" + title: "Tutorial" + wellDone: "Gut gemacht!" + skipAreYouSure: "Möchtest du das Tutorial verlassen?" + _landing: + title: "Willkommen zum Tutorial" + description: "Hier kannst du sehen, wie Misskey funktioniert" + _note: + title: "Was sind Notizen?" _serverRules: description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen." _serverSettings: diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 9392fd12fe..30a52b726e 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -104,7 +104,6 @@ clickToShow: "Κάντε κλικ για εμφάνιση" add: "Προσθέστε" reaction: "Αντιδράσεις" reactions: "Αντιδράσεις" -reactionSetting: "Αντιδράσεις για εμφάνιση στην επιλογή αντίδρασης" reactionSettingDescription2: "Σύρετε για να αλλάξετε τη σειρά, κάντε κλικ για να διαγράψετε, πατήστε \"+\" για να προσθέσετε." rememberNoteVisibility: "Θυμήσου τις ρυθμίσεις ορατότητας σημειώματος" attachCancel: "Διαγραφή αρχείου" @@ -228,7 +227,6 @@ userList: "Λίστες" about: "Πληροφορίες" moderator: "Συντονιστής" moderation: "Συντονισμός" -cacheClear: "Εκκαθάριση προσωρινής μνήμης" markAsReadAllNotifications: "Όλες οι ειδοποιήσεις διαβάστηκαν" members: "Μέλη" transfer: "Μεταφορά" diff --git a/locales/en-US.yml b/locales/en-US.yml index 7f5239ae8d..64f5d568eb 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -46,7 +46,7 @@ pin: "Pin to profile" unpin: "Unpin from profile" copyContent: "Copy contents" copyLink: "Copy link" -copyLinkRenote: "Copy renote link" +copyLinkRenote: "Copy boost link" delete: "Delete" deleteAndEdit: "Delete and edit" deleteAndEditConfirm: "Are you sure you want to redraft this note? This means you will lose all reactions, boosts, and replies to it." @@ -115,7 +115,7 @@ rmboost: "Unboosted." cantRenote: "This post can't be boosted." cantReRenote: "A boost can't be boosted." quote: "Quote" -inChannelRenote: "Channel-only Renote" +inChannelRenote: "Channel-only Boost" inChannelQuote: "Channel-only Quote" pinnedNote: "Pinned note" pinned: "Pin to profile" @@ -125,7 +125,12 @@ sensitive: "Sensitive" add: "Add" reaction: "Reactions" reactions: "Reactions" -reactionSetting: "Reactions to show in the reaction picker" +emojiPicker: "Emoji picker" +pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately when reacting." +pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when entering emojis" +emojiPickerDisplay: "Emoji picker display" +overwriteFromPinnedEmojisForReaction: "Override from reaction settings" +overwriteFromPinnedEmojis: "Override from general settings" reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add." rememberNoteVisibility: "Remember note visibility settings" attachCancel: "Remove attachment" @@ -134,8 +139,8 @@ unmarkAsSensitive: "Unmark as sensitive" enterFileName: "Enter filename" mute: "Mute" unmute: "Unmute" -renoteMute: "Mute Renotes" -renoteUnmute: "Unmute Renotes" +renoteMute: "Mute Boosts" +renoteUnmute: "Unmute Boosts" block: "Block" unblock: "Unblock" markAsNSFW: "Mark all media from user as NSFW" @@ -271,6 +276,7 @@ removed: "Successfully deleted" removeAreYouSure: "Are you sure that you want to remove \"{x}\"?" deleteAreYouSure: "Are you sure that you want to delete \"{x}\"?" resetAreYouSure: "Really reset?" +areYouSure: "Are you sure?" saved: "Saved" messaging: "Chat" upload: "Upload" @@ -321,6 +327,7 @@ folderName: "Folder name" createFolder: "Create a folder" renameFolder: "Rename this folder" deleteFolder: "Delete this folder" +folder: "Folder" addFile: "Add a file" emptyDrive: "Your Drive is empty" emptyFolder: "This folder is empty" @@ -448,7 +455,6 @@ share: "Share" notFound: "Not found" notFoundDescription: "No page corresponding to this URL could be found." uploadFolder: "Default folder for uploads" -cacheClear: "Clear cache" markAsReadAllNotifications: "Mark all notifications as read" markAsReadAllUnreadNotes: "Mark all notes as read" markAsReadAllTalkMessages: "Mark all messages as read" @@ -558,6 +564,8 @@ showInPage: "Show in page" popout: "Pop-out" volume: "Volume" masterVolume: "Master volume" +notUseSound: "Disable sound" +useSoundOnlyWhenActive: "Output sounds only if Sharkey is active." details: "Details" chooseEmoji: "Select an emoji" unableToProcess: "The operation could not be completed" @@ -578,6 +586,10 @@ output: "Output" script: "Script" disablePagesScript: "Disable AiScript on Pages" updateRemoteUser: "Update remote user information" +unsetUserAvatar: "Unset avatar" +unsetUserAvatarConfirm: "Are you sure you want to unset the avatar?" +unsetUserBanner: "Unset banner" +unsetUserBannerConfirm: "Are you sure you want to unset the banner?" deleteAllFiles: "Delete all files" deleteAllFilesConfirm: "Are you sure that you want to delete all files?" removeAllFollowing: "Unfollow all followed users" @@ -649,6 +661,7 @@ smtpSecure: "Use implicit SSL/TLS for SMTP connections" smtpSecureInfo: "Turn this off when using STARTTLS" testEmail: "Test email delivery" wordMute: "Word mute" +hardWordMute: "Hard word mute" regexpError: "Regular Expression error" regexpErrorDescription: "An error occurred in the regular expression on line {line} of your {tab} word mutes:" instanceMute: "Instance Mutes" @@ -676,7 +689,7 @@ behavior: "Behavior" sample: "Sample" abuseReports: "Reports" reportAbuse: "Report" -reportAbuseRenote: "Report renote" +reportAbuseRenote: "Report boost" reportAbuseOf: "Report {name}" fillAbuseReportDescription: "Please fill in details regarding this report. If it is about a specific note, please include its URL." abuseReported: "Your report has been sent. Thank you very much." @@ -885,8 +898,8 @@ makeReactionsPublicDescription: "This will make the list of all your past reacti classic: "Classic" muteThread: "Mute thread" unmuteThread: "Unmute thread" -ffVisibility: "Follows/Followers Visibility" -ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you." +followingVisibility: "Visibility of follows" +followersVisibility: "Visibility of followers" continueThread: "View thread continuation" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" incorrectPassword: "Incorrect password." @@ -944,6 +957,8 @@ approvalStatus: "Approval Status" document: "Documentation" numberOfPageCache: "Number of cached pages" numberOfPageCacheDescription: "Increasing this number will improve convenience for but cause more load as more memory usage on the user's device." +numberOfReplies: "Number of replies in a thread" +numberOfRepliesDescription: "Increasing this number will display more replies. Setting this too high can cause replies to be cramped and unreadable." logoutConfirm: "Really log out?" lastActiveDate: "Last used at" statusbar: "Status bar" @@ -1048,6 +1063,8 @@ resetPasswordConfirm: "Really reset your password?" 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." sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." +hiddenTags: "Hidden hashtags" +hiddenTagsDescription: "Select tags which will not shown on trend list.\nMultiple tags could be registered by lines." notesSearchNotAvailable: "Note search is unavailable." license: "License" unfavoriteConfirm: "Really remove from favorites?" @@ -1060,6 +1077,7 @@ enableChartsForRemoteUser: "Generate remote user data charts" enableChartsForFederatedInstances: "Generate remote instance data charts" showClipButtonInNoteFooter: "Add \"Clip\" to note action menu" reactionsDisplaySize: "Reaction display size" +limitWidthOfReaction: "Limits the maximum width of reactions and display them in reduced size." noteIdOrUrl: "Note ID or URL" video: "Video" videos: "Videos" @@ -1181,6 +1199,7 @@ tosAndPrivacyPolicy: "Terms of Service and Privacy Policy" avatarDecorations: "Avatar decorations" attach: "Attach" detach: "Remove" +detachAll: "Remove all" angle: "Angle" flip: "Flip" showAvatarDecorations: "Show avatar decorations" @@ -1192,6 +1211,14 @@ useGroupedNotifications: "Display grouped notifications" signupPendingError: "There was a problem verifying the email address. The link may have expired." cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided." doReaction: "Add reaction" +code: "Code" +reloadRequiredToApplySettings: "Reloading is required to apply the settings." +remainingN: "Remaining: {n}" +overwriteContentConfirm: "Are you sure you want to overwrite the current content?" +seasonalScreenEffect: "Seasonal screen effects" +decorate: "Decorate" +addMfmFunction: "Add MFM" +enableQuickAddMfmFunction: "Show advanced MFM picker" _announcement: forExistingUsers: "Existing users only" forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." @@ -1258,8 +1285,8 @@ _initialTutorial: _visibility: description: "You can limit who can view your note." public: "Your note will be visible for all users." - home: "Public only on the Home timeline. People visiting your profile, via followers, and through renotes can see it." - followers: "Visible to followers only. Only followers can see it and no one else, and it cannot be renoted by others." + home: "Public only on the Home timeline. People visiting your profile, via followers, and through boosts can see it." + followers: "Visible to followers only. Only followers can see it and no one else, and it cannot be boosted by others." direct: "Visible only to specified users, and the recipient will be notified. It can be used as an alternative to direct messaging." doNotSendConfidencialOnDirect1: "Be careful when sending sensitive information!" doNotSendConfidencialOnDirect2: "Administrators of the server can see what you write. Be careful with sensitive information when sending direct notes to users on untrusted servers." @@ -1301,7 +1328,7 @@ _serverSettings: shortNameDescription: "A shorthand for the instance's name that can be displayed if the full official name is long." fanoutTimelineDescription: "Greatly increases performance of timeline retrieval and reduces load on the database when enabled. In exchange, memory usage of Redis will increase. Consider disabling this in case of low server memory or server instability." fanoutTimelineDbFallback: "Fallback to database" - fanoutTimelineDbFallbackDescription: "When enabled, fallback processing is performed by making an additional query to the DB if the timeline is not cached. Disabling it further reduces the server load by not performing fallback processing, but limits the range of timelines that can be retrieved." + fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved." _accountMigration: moveFrom: "Migrate another account to this one" moveFromSub: "Create alias to another account" @@ -1572,7 +1599,9 @@ _role: assignTarget: "Assignment type" descriptionOfAssignTarget: "Manual to manually change who is part of this role and who is not.\nConditional to have users be automatically assigned and removed from this role based on a condition." manual: "Manual" + manualRoles: "Manual roles" conditional: "Conditional" + conditionalRoles: "Conditional roles" condition: "Condition" isConditionalRole: "This is a conditional role." isPublic: "Public role" @@ -1598,6 +1627,7 @@ _role: high: "High" _options: gtlAvailable: "Can view the global timeline" + btlAvailable: "Can view the bubble timeline" ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" canImportNotes: "Can import notes" @@ -1622,6 +1652,7 @@ _role: canHideAds: "Can hide ads" canSearchNotes: "Usage of note search" canUseTranslator: "Translator usage" + avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" _condition: isLocal: "Local user" isRemote: "Remote user" @@ -1650,6 +1681,7 @@ _emailUnavailable: disposable: "Disposable email addresses may not be used" mx: "This email server is invalid" smtp: "This email server is not responding" + banned: "This email address is banned" _ffVisibility: public: "Public" followers: "Visible to followers only" @@ -1754,7 +1786,7 @@ _channel: notesCount: "{n} Notes" nameAndDescription: "Name and description" nameOnly: "Name only" - allowRenoteToExternal: "Allow renote and quote outside the channel" + allowRenoteToExternal: "Allow boosts and quote outside the channel" _menuDisplay: sideFull: "Side" sideIcon: "Side (Icons)" @@ -1817,7 +1849,7 @@ _theme: hashtag: "Hashtag" mention: "Mention" mentionMe: "Mentions (Me)" - renote: "Renote" + renote: "Boost" modalBg: "Modal background" divider: "Divider" scrollbarHandle: "Scrollbar handle" @@ -1846,6 +1878,14 @@ _sfx: notification: "Notifications" antenna: "Antennas" channel: "Channel notifications" + reaction: "On choosing a reaction" +_soundSettings: + driveFile: "Use an audio file in Drive." + driveFileWarn: "Select an audio file from Drive." + driveFileTypeWarn: "This file is not supported" + driveFileTypeWarnDescription: "Select an audio file" + driveFileDurationWarn: "The audio is too long." + driveFileDurationWarnDescription: "Long audio may disrupt using Sharkey. Still continue?" _ago: future: "Future" justNow: "Just now" @@ -1858,13 +1898,13 @@ _ago: yearsAgo: "{n}y ago" invalid: "None" _timeIn: - seconds: "in {n} seconds" - minutes: "in {n} minutes" - hours: "in {n} hours" - days: "in {n} days" - weeks: "in {n} weeks" - months: "in {n} months" - years: "in {n} years" + seconds: "In {n}s" + minutes: "In {n}m" + hours: "In {n}h" + days: "In {n}d" + weeks: "In {n}w" + months: "In {n}mo" + years: "In {n}y" _time: second: "Second(s)" minute: "Minute(s)" @@ -1936,6 +1976,55 @@ _permissions: "write:flash": "Edit Plays" "read:flash-likes": "View list of liked Plays" "write:flash-likes": "Edit list of liked Plays" + "read:admin:abuse-user-reports": "View user reports" + "write:admin:delete-account": "Delete account" + "write:admin:delete-all-files-of-a-user": "Delete all files of a user" + "read:admin:index-stats": "View information about database indexes" + "read:admin:table-stats": "View information about database tables" + "read:admin:user-ips": "View user IP address" + "read:admin:meta": "View instance metadata" + "write:admin:reset-password": "Reset user passwords" + "write:admin:resolve-abuse-user-report": "Resolve user reports" + "write:admin:send-email": "Send Email" + "read:admin:server-info": "View server info" + "read:admin:show-moderation-log": "View moderation log" + "read:admin:show-user": "View user information" + "read:admin:show-users": "View users" + "write:admin:suspend-user": "Suspend user" + "write:admin:unset-user-avatar": "Remove avatar from user" + "write:admin:unset-user-banner": "Remove banner from user" + "write:admin:unsuspend-user": "Unsuspend user" + "write:admin:meta": "Edit instance metadata" + "write:admin:user-note": "Edit user note" + "write:admin:roles": "Edit roles" + "read:admin:roles": "View roles" + "write:admin:relays": "Edit relays" + "read:admin:relays": "View relays" + "write:admin:invite-codes": "Edit invite codes" + "read:admin:invite-codes": "View invite codes" + "write:admin:announcements": "Edit announcements" + "read:admin:announcements": "View announcements" + "write:admin:avatar-decorations": "Edit avatar decorations" + "read:admin:avatar-decorations": "View avatar decorations" + "write:admin:federation": "Edit remote instance information" + "write:admin:account": "Edit users" + "read:admin:account": "View information about user" + "write:admin:emoji": "Edit emojis" + "read:admin:emoji": "View emojis" + "write:admin:queue": "Edit queue" + "read:admin:queue": "View queue" + "write:admin:promo": "Edit promo" + "write:admin:drive": "Edit user drive" + "read:admin:drive": "View user drive" + "read:admin:stream": "Using the Websocket API for Admin" + "write:admin:ad": "Edit ads" + "read:admin:ad": "View ads" + "write:invite-codes": "Create Invitation Code" + "read:invite-codes": "View Invitation Code" + "write:clip-favorite": "Edit clips and likes" + "read:clip-favorite": "View clips and likes" + "read:federation": "View information about remote instance" + "write:report-abuse": "Report abuse" _auth: shareAccessTitle: "Granting application permissions" shareAccess: "Would you like to authorize \"{name}\" to access this account?" @@ -1991,6 +2080,7 @@ _widgets: chooseList: "Select a list" clicker: "Clicker" search: "Search" + birthdayFollowings: "Users who celebrate their birthday today" _cw: hide: "Hide" show: "Show content" @@ -2055,6 +2145,7 @@ _profile: changeBanner: "Change banner" changeBackground: "Change background" verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field." + avatarDecorationMax: "You can add up to {max} decorations." _exportOrImport: allNotes: "All notes" favoritedNotes: "Favorite notes" @@ -2176,6 +2267,7 @@ _notification: pollEnded: "Poll results have become available" newNote: "New note" unreadAntennaNote: "Antenna {name}" + roleAssigned: "Role given" emptyPushNotificationMessage: "Push notifications have been updated" achievementEarned: "Achievement unlocked" testNotification: "Test notification" @@ -2183,7 +2275,7 @@ _notification: sendTestNotification: "Send test notification" notificationWillBeDisplayedLikeThis: "Notifications look like this" reactedBySomeUsers: "{n} users reacted" - renotedBySomeUsers: "Renote from {n} users" + renotedBySomeUsers: "Boosted by {n} users" followedBySomeUsers: "Followed by {n} users" _types: all: "All" @@ -2191,18 +2283,19 @@ _notification: follow: "New followers" mention: "Mentions" reply: "Replies" - renote: "Renotes" + renote: "Boosts" quote: "Quotes" reaction: "Reactions" pollEnded: "Polls ending" receiveFollowRequest: "Received follow requests" followRequestAccepted: "Accepted follow requests" + roleAssigned: "Role given" achievementEarned: "Achievement unlocked" app: "Notifications from linked apps" _actions: followBack: "followed you back" reply: "Reply" - renote: "Renote" + renote: "Boost" _deck: alwaysShowMainColumn: "Always show main column" columnAlign: "Align columns" @@ -2292,6 +2385,8 @@ _moderationLogTypes: createAvatarDecoration: "Avatar decoration created" updateAvatarDecoration: "Avatar decoration updated" deleteAvatarDecoration: "Avatar decoration deleted" + unsetUserAvatar: "Unset this user's avatar" + unsetUserBanner: "Unset this user's banner" _mfm: intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax." dummy: "Sharkey expands the world of the Fediverse" @@ -2427,3 +2522,17 @@ _dataRequest: warn: "Data requests are only possible every 3 days." text: "Once the data is ready to download, an email will be sent to the email address registered to this account." button: "Request" + +_dataSaver: + _media: + title: "Loading Media" + description: "Prevents images/videos from being loaded automatically. Hidden images/videos will be loaded when tapped." + _avatar: + title: "Avatar image" + description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic." + _urlPreview: + title: "URL preview thumbnails" + description: "URL preview thumbnail images will no longer be loaded." + _code: + title: "Code highlighting" + description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 0fff937d84..c269cc4d75 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -121,7 +121,12 @@ sensitive: "Marcado como sensible" add: "Agregar" reaction: "Reacción" reactions: "Reacción" -reactionSetting: "Reacciones para mostrar en el menú de reacciones" +emojiPicker: "Selector de emojis" +pinnedEmojisForReactionSettingDescription: "Puedes seleccionar reacciones para fijarlos en el selector" +pinnedEmojisSettingDescription: "Puedes seleccionar emojis para fijarlos en el selector" +emojiPickerDisplay: "Mostrar el selector de emojis" +overwriteFromPinnedEmojisForReaction: "Sobreescribir las reacciones fijadas" +overwriteFromPinnedEmojis: "Sobreescribir los emojis fijados" reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir." rememberNoteVisibility: "Recordar visibilidad" attachCancel: "Quitar adjunto" @@ -261,6 +266,7 @@ removed: "Borrado" removeAreYouSure: "¿Desea borrar \"{x}\"?" deleteAreYouSure: "¿Desea borrar \"{x}\"?" resetAreYouSure: "¿Desea reestablecer?" +areYouSure: "¿Estás conforme?" saved: "Guardado" messaging: "Chat" upload: "Subir" @@ -311,6 +317,7 @@ folderName: "Nombre de la carpeta" createFolder: "Crear carpeta" renameFolder: "Renombrar carpeta" deleteFolder: "Borrar carpeta" +folder: "Carpeta" addFile: "Agregar archivo" emptyDrive: "El drive está vacío" emptyFolder: "La carpeta está vacía" @@ -437,7 +444,6 @@ share: "Compartir" notFound: "No se encuentra" notFoundDescription: "No se encontró la página correspondiente a la URL elegida" uploadFolder: "Carpeta de subidas por defecto" -cacheClear: "Borrar caché" markAsReadAllNotifications: "Marcar todas las notificaciones como leídas" markAsReadAllUnreadNotes: "Marcar todas las notas como leídas" markAsReadAllTalkMessages: "Marcar todos los chats como leídos" @@ -544,6 +550,8 @@ showInPage: "Mostrar en la página" popout: "Popout" volume: "Volumen" masterVolume: "Volumen principal" +notUseSound: "Sin sonido" +useSoundOnlyWhenActive: "Sonar solo cuando Misskey esté activo" details: "Detalles" chooseEmoji: "Elije un emoji" unableToProcess: "La operación no se puede llevar a cabo" @@ -564,6 +572,10 @@ output: "Salida" script: "Script" disablePagesScript: "Deshabilitar AiScript en Páginas" updateRemoteUser: "Actualizar información de usuario remoto" +unsetUserAvatar: "Quitar avatar" +unsetUserAvatarConfirm: "¿Confirmas que quieres quitar tu avatar?" +unsetUserBanner: "Quitar banner" +unsetUserBannerConfirm: "¿Confirmas que quieres quitar tu banner?" deleteAllFiles: "Borrar todos los archivos" deleteAllFilesConfirm: "¿Desea borrar todos los archivos?" removeAllFollowing: "Retener todos los siguientes" @@ -635,6 +647,7 @@ smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP" smtpSecureInfo: "Apagar cuando se use STARTTLS" testEmail: "Prueba de envío" wordMute: "Silenciar palabras" +hardWordMute: "Filtro de palabra fuerte" regexpError: "Error de la expresión regular" regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line} de las palabras muteadas {tab}" instanceMute: "Instancias silenciadas" @@ -868,8 +881,8 @@ makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán pú classic: "Clásico" muteThread: "Silenciar hilo" unmuteThread: "Mostrar hilo" -ffVisibility: "Visibilidad de seguidores y seguidos" -ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes te siguen" +followingVisibility: "Visibilidad de seguidos" +followersVisibility: "Visibilidad de seguidores" continueThread: "Ver la continuación del hilo" deleteAccountConfirm: "La cuenta será borrada. ¿Está seguro?" incorrectPassword: "La contraseña es incorrecta" @@ -979,6 +992,7 @@ assign: "Asignar" unassign: "Quitar" color: "Color" manageCustomEmojis: "Administrar emojis personalizados" +manageAvatarDecorations: "Administrar decoraciones de avatar" youCannotCreateAnymore: "Has llegado al límite de creaciones." cannotPerformTemporary: "Temporalmente no disponible" cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo." @@ -1019,6 +1033,8 @@ resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?" sensitiveWords: "Palabras sensibles" sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea" sensitiveWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares." +hiddenTags: "Hashtags ocultos" +hiddenTagsDescription: "Selecciona las etiquetas que no se mostrarán en tendencias. Una etiqueta por línea." notesSearchNotAvailable: "No se puede buscar una nota" license: "Licencia" unfavoriteConfirm: "¿Desea quitar de favoritos?" @@ -1031,6 +1047,7 @@ enableChartsForRemoteUser: "Generar gráficas de usuarios remotos." enableChartsForFederatedInstances: "Generar gráficos de servidores remotos" showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas" reactionsDisplaySize: "Tamaño de las reacciones" +limitWidthOfReaction: "Limitar ancho de las reacciones" noteIdOrUrl: "ID o URL de la nota" video: "Video" videos: "Video" @@ -1132,6 +1149,10 @@ mutualFollow: "Os seguís mutuamente" fileAttachedOnly: "Solo notas con archivos" showRepliesToOthersInTimeline: "Mostrar respuestas a otros en la línea de tiempo" hideRepliesToOthersInTimeline: "Ocultar respuestas a otros en la línea de tiempo" +showRepliesToOthersInTimelineAll: "Muestra tus respuestas a otros usuarios que sigues en la línea de tiempo" +hideRepliesToOthersInTimelineAll: "Ocultar tus respuestas a otros usuarios que sigues en la 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?" externalServices: "Servicios Externos" impressum: "Impressum" impressumUrl: "Impressum URL" @@ -1139,7 +1160,27 @@ impressumDescription: "En algunos países, como Alemania, la inclusión del oper privacyPolicy: "Política de Privacidad" privacyPolicyUrl: "URL de la Política de Privacidad" tosAndPrivacyPolicy: "Condiciones de Uso y Política de Privacidad" +avatarDecorations: "Decoraciones de avatar" +attach: "Acoplar" +detach: "Quitar" +detachAll: "Quitar todo" +angle: "Ángulo" flip: "Echar de un capirotazo" +showAvatarDecorations: "Mostrar decoraciones de avatar" +releaseToRefresh: "Soltar para recargar" +refreshing: "Recargando..." +pullDownToRefresh: "Tira hacia abajo para recargar" +disableStreamingTimeline: "Desactivar actualizaciones en tiempo real de la línea de tiempo" +useGroupedNotifications: "Mostrar notificaciones agrupadas" +signupPendingError: "Ha habido un problema al verificar tu dirección de correo electrónico. Es posible que el enlace haya caducado." +cwNotationRequired: "Si se ha activado \"ocultar contenido\", es necesario proporcionar una descripción." +doReaction: "Añadir reacción" +code: "Código" +reloadRequiredToApplySettings: "Es necesario recargar para que se aplique la configuración." +remainingN: "Faltan: {n}" +overwriteContentConfirm: "¿Quieres sustituir todo el contenido actual?" +seasonalScreenEffect: "Efectos de pantalla asociados a estaciones" +decorate: "Decorar" _announcement: forExistingUsers: "Solo para usuarios registrados" forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán." @@ -1149,6 +1190,10 @@ _announcement: tooManyActiveAnnouncementDescription: "Tener demasiados anuncios activos empeora la experiencia de usuario. Por favor, considera archivar aquellos anuncios que hayan quedado obsoletos." readConfirmTitle: "¿Marcar como leído?" readConfirmText: "Esto marcará el contenido de \"{title}\" como leído." + shouldNotBeUsedToPresentPermanentInfo: "Dado que puede impactar en la experiencia de usuario de forma significativa, es recomendable usar notificaciones en el flujo de información en vez de información persistente." + dialogAnnouncementUxWarn: "Mostrar dos o más notificaciones en formato diálogo a la vez puede impactar en la experiencia de usuario de forma significativa, úsalos con cuidado." + silence: "Silenciar notificaciones" + silenceDescription: "Si lo activas, no enviarás notificación sobre este anuncio y el usuario no tendrá que leerlo." _initialAccountSetting: accountCreated: "¡La cuenta ha sido creada!" letsStartAccountSetup: "Para empezar, creemos tu perfil." @@ -1161,8 +1206,77 @@ _initialAccountSetting: pushNotificationDescription: "Habilitar las notificaciones push te permitirá recibir notificaciones de {name} directamente en tu dispositivo." initialAccountSettingCompleted: "¡Configuración del perfil completada!" haveFun: "¡Disfruta de {name}!" + youCanContinueTutorial: "Puedes proceder a un tutorial sobre cómo usar {name} (Misskey) o puedes terminar la instalación aquí y empezar a usarlo ya mismo." + startTutorial: "Comenzar tutorial" skipAreYouSure: "¿Realmente quieres saltarte la configuración del perfil?" laterAreYouSure: "¿Realmente quieres configurar tu perfil después?" +_initialTutorial: + launchTutorial: "Comenzar tutorial" + title: "Tutorial" + wellDone: "¡Bien hecho!" + skipAreYouSure: "¿Salir del tutorial?" + _landing: + title: "Bienvenid@ al tutorial" + description: "Aquí podrás aprender las nociones básicas sobre cómo usar Misskey y sus funciones." + _note: + title: "¿Qué es una nota?" + description: "Las publicaciones en Misskey se llaman 'Notas'. Las notas se ordenan de forma cronológica en la línea de tiempo y se actualizan en tiempo real." + reply: "Pulsa en este botón para contestar a un mensaje. También es posible contestar a otras contestaciones, continuando así la conversación como un hilo." + renote: "Puedes compartir esa nota en tu propia línea de tiempo. También puedes añadir una cita con tus comentarios." + reaction: "Puedes añadir reacciones a la Nota. Se explicarán más detalles en la siguiente página." + menu: "Puedes ver los detalles de la Nota, copiar enlaces, y realizar otras acciones." + _reaction: + title: "¿Qué son las reacciones?" + description: "Se puede reaccionar a las Notas con diferentes emojis. Las reacciones te permiten expresar matices que no se pueden transmitir con un simple 'me gusta'." + letsTryReacting: "Puedes añadir reacciones pulsando en el botón '+' de la nota. ¡Intenta reaccionar a esta nota de ejemplo!" + reactToContinue: "Añade una reacción para continuar." + reactNotification: "Recibirás notificaciones en tiempo real cuando alguien reaccione a tu nota." + reactDone: "Puedes deshacer una reacción pulsando en el botón '-'." + _timeline: + title: "El concepto de Línea de tiempo" + description1: "Misskey proporciona múltiples líneas de tiempo basadas en su uso (algunas pueden no estar disponibles dependiendo de las políticas de la instancia)." + home: "Puedes ver los posts de las cuentas que sigues." + local: "Puedes ver los posts de todos los usuarios de este servidor." + social: "Se ven los posts de la línea de tiempo de inicio junto con los de la línea de tiempo local." + global: "Puedes ver notas de todos los servidores conectados." + description2: "Puedes cambiar la línea de tiempo en la parte superior de la pantalla cuando quieras." + description3: "Además, hay listas de líneas de tiempo y listas de canales. Para más detalle, por favor visita este enlace: {link}" + _postNote: + title: "Ajustes de publicación de nota" + description1: "Cuando publicas una nota en Misskey, hay varias opciones disponibles. El formulario tiene este aspecto." + _visibility: + description: "Puedes limitar quién puede ver tu nota." + public: "Tu nota será visible para todos los usuarios." + home: "Publicar solo en la línea de tiempo de Inicio. La nota se verá en tu perfil, la verán tus seguidores y también cuando sea renotada." + followers: "Visible solo para seguidores. Sólo tus seguidores podrán ver la nota, y no podrá ser renotada por otras personas." + direct: "Visible sólo para usuarios específicos, y el destinatario será notificado. Puede usarse como alternativa a la mensajería directa." + doNotSendConfidencialOnDirect1: "¡Ten cuidado cuando vayas a enviar información sensible!" + doNotSendConfidencialOnDirect2: "Los administradores del servidor pueden leer lo que escribes. Ten cuidado cuando envíes información sensible en notas directas en servidores no confiables." + localOnly: "Publicando con esta opción seleccionada, la nota no se federará hacia otros servidores. Los usuarios de otros servidores no podrán ver estas notas directamente, sin importar los ajustes seleccionados más arriba." + _cw: + title: "Alerta de contenido (CW)" + description: "En lugar de mostrarse el contenido de la nota, se mostrará lo que escribas en el campo \"comentarios\". Pulsando en \"leer más\" desplegará el contenido de la nota." + _exampleNote: + cw: "¡Esto te hará tener hambre!" + note: "Acabo de comerme un donut de chocolate glaseado 🍩😋" + useCases: "Esto se usa cuando las normas del servidor lo requieren, o para ocultar spoilers o contenido sensible." + _howToMakeAttachmentsSensitive: + title: "¿Cómo puedo marcar adjuntos como contenido sensible?" + description: "Cuando las normas del servidor lo requieran, o el contenido lo requiera, marca la opción de \"contenido sensible\" para el adjunto." + tryThisFile: "¡Prueba a marcar la imagen adjunta como contenido sensible!" + _exampleNote: + note: "Ups, la he liado al abrir la tapa del natto..." + method: "Para marcar un adjunto como sensible, haz clic en la miniatura, abre el menú, y haz clic en \"Marcar como sensible\"." + sensitiveSucceeded: "Cuando adjuntes archivos, por favor, ten en cuenta las normas del servidor para marcarlos como contenido sensible." + doItToContinue: "Marca el archivo adjunto como sensible para continuar." + _done: + title: "¡Has completado el tutorial! 🎉" + description: "Las funciones que mostramos aquí son sólo una pequeña parte. Para más detalles sobre el funcionamiento de Misskey, pulsa en este enlace: {link}" +_timelineDescription: + home: "En la línea de tiempo de Inicio puedes ver las notas de las cuentas a las que sigues." + local: "En la línea de tiempo Local puedes ver las notas de todos los usuarios del servidor." + social: "En la línea de tiempo Social verás las notas de Inicio y Local a la vez." + global: "En la línea de tiempo Global verás las notas de todos los servidores conectados." _serverRules: description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado." _serverSettings: @@ -1174,6 +1288,9 @@ _serverSettings: manifestJsonOverride: "Sobreescribir manifest.json" shortName: "Nombre corto" shortNameDescription: "Forma corta del nombre de la instancia que puede mostrarse si el nombre completo es demasiado largo." + fanoutTimelineDescription: "Incrementa el rendimiento de forma significativa cuando se obtienen las líneas de tiempo y reduce la carga en la base de datos. A cambio, el uso de la memoria en Redis incrementará. Considera desactivar esta opción en caso de que tu servidor tenga poca memoria o detectes inestabilidad." + fanoutTimelineDbFallback: "Cargar desde la base de datos" + fanoutTimelineDbFallbackDescription: "Cuando esta opción está habilitada, la carga de peticiones adicionales de la línea de tiempo se hará desde la base de datos cuando éstas no se encuentren en la caché. Al deshabilitar esta opción se reduce la carga del servidor, pero limita el número de líneas de tiempo que pueden obtenerse." _accountMigration: moveFrom: "Trasladar de otra cuenta a ésta" moveFromSub: "Crear un alias para otra cuenta." @@ -1431,6 +1548,9 @@ _achievements: _smashTestNotificationButton: title: "Sobrecarga de pruebas" description: "Envía muchas notificaciones de prueba en un corto espacio de tiempo" + _tutorialCompleted: + title: "Diploma del Curso Básico de Misskey" + description: "Tutorial completado" _role: new: "Crear rol" edit: "Editar rol" @@ -1441,7 +1561,9 @@ _role: assignTarget: "Asignar objetivo" descriptionOfAssignTarget: "Manual Para cambiar manualmente lo que se incluye en este rol.\nCondicional configura una condición, y los usuarios que cumplan la condición serán incluídos automáticamente." manual: "manual" + manualRoles: "Roles manuales" conditional: "condicional" + conditionalRoles: "Roles condicionales" condition: "condición" isConditionalRole: "Esto es un rol condicional" isPublic: "Publicar rol" @@ -1474,6 +1596,7 @@ _role: inviteLimitCycle: "Enfriamiento del límite de invitaciones" inviteExpirationTime: "Intervalo de caducidad de invitaciones" canManageCustomEmojis: "Administrar emojis personalizados" + canManageAvatarDecorations: "Administrar decoraciones de avatar" driveCapacity: "Capacidad del drive" alwaysMarkNsfw: "Siempre marcar archivos como NSFW" pinMax: "Máximo de notas fijadas" @@ -1489,6 +1612,7 @@ _role: canHideAds: "Puede ocultar anuncios" canSearchNotes: "Uso de la búsqueda de notas" canUseTranslator: "Uso de traductor" + avatarDecorationLimit: "Número máximo de decoraciones de avatar" _condition: isLocal: "Usuario local" isRemote: "Usuario remoto" @@ -1517,6 +1641,7 @@ _emailUnavailable: disposable: "No es un correo reutilizable" mx: "Servidor de correo inválido" smtp: "Servidor de correo no disponible" + banned: "Email no disponible" _ffVisibility: public: "Publicar" followers: "Visible solo para seguidores" @@ -1593,6 +1718,7 @@ _aboutMisskey: donate: "Donar a Misskey" morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰" patrons: "Patrocinadores" + projectMembers: "Miembros del proyecto" _displayOfSensitiveMedia: respect: "Esconder medios marcados como sensibles" ignore: "Mostrar medios marcados como sensibles" @@ -1617,6 +1743,7 @@ _channel: notesCount: "{n} notas" nameAndDescription: "Nombre y descripción" nameOnly: "Sólo nombre" + allowRenoteToExternal: "Permitir renotas y menciones fuera del canal" _menuDisplay: sideFull: "Horizontal" sideIcon: "Horizontal (ícono)" @@ -1708,6 +1835,14 @@ _sfx: notification: "Notificaciones" antenna: "Antena receptora" channel: "Notificaciones del canal" + reaction: "Al seleccionar una reacción" +_soundSettings: + driveFile: "Usar un archivo de audio en Drive" + driveFileWarn: "Selecciona un archivo de audio en Drive." + driveFileTypeWarn: "Este archivo es incompatible" + driveFileTypeWarnDescription: "Selecciona un archivo de audio" + driveFileDurationWarn: "La duración del audio es demasiado larga." + driveFileDurationWarnDescription: "Usar un audio de larga duración puede llegar a molestar mientras usas Misskey. ¿Quieres continuar?" _ago: future: "Futuro" justNow: "Justo ahora" @@ -1719,6 +1854,14 @@ _ago: monthsAgo: "Hace {n} meses" yearsAgo: "Hace {n} años" invalid: "No hay nada que ver aqui" +_timeIn: + seconds: "En {n} segundos" + minutes: "En {n}m" + hours: "En {n}h" + days: "En {n}d" + weeks: "En {n}sem." + months: "En {n}M" + years: "En {n} años" _time: second: "Segundos" minute: "Minutos" @@ -1844,6 +1987,7 @@ _widgets: _userList: chooseList: "Seleccione una lista" clicker: "Cliqueador" + birthdayFollowings: "Hoy cumplen años" _cw: hide: "Ocultar" show: "Ver más" @@ -1871,6 +2015,7 @@ _poll: remainingHours: "Quedan {h} horas y {m} minutos para que finalice" remainingMinutes: "Quedan {m} minutos y {s} segundos para que finalice" remainingSeconds: "Quedan {s} segundos para que finalice" + multiple: "Opciones múltiples" _visibility: public: "Público" publicDescription: "Visible para todos los usuarios" @@ -1906,6 +2051,7 @@ _profile: changeAvatar: "Cambiar avatar" changeBanner: "Cambiar banner" verifiedLinkDescription: "Introduciendo una URL que contiene un enlace a tu perfil, se puede mostrar un icono de verificación de propiedad al lado del campo." + avatarDecorationMax: "Puedes añadir un máximo de {max} decoraciones de avatar." _exportOrImport: allNotes: "Todas las notas" favoritedNotes: "Notas favoritas" @@ -2027,12 +2173,16 @@ _notification: pollEnded: "Estan disponibles los resultados de la encuesta" newNote: "Nueva nota" unreadAntennaNote: "Antena {name}" + roleAssigned: "Rol asignado" emptyPushNotificationMessage: "Se han actualizado las notificaciones push" achievementEarned: "Logro desbloqueado" testNotification: "Notificación de prueba" checkNotificationBehavior: "Comprobar comportamiento de la notificación" sendTestNotification: "Enviar notificación de prueba" notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto" + reactedBySomeUsers: "{n} usuarios han reaccionado" + renotedBySomeUsers: "{n} usuarios han renotado" + followedBySomeUsers: "Seguido por {n} usuarios" _types: all: "Todo" note: "Nuevas notas" @@ -2045,6 +2195,7 @@ _notification: pollEnded: "La encuesta terminó" receiveFollowRequest: "Recibió una solicitud de seguimiento" followRequestAccepted: "El seguimiento fue aceptado" + roleAssigned: "Rol asignado" achievementEarned: "Logro desbloqueado" app: "Notificaciones desde aplicaciones" _actions: @@ -2136,6 +2287,11 @@ _moderationLogTypes: createAd: "Anuncio creado" deleteAd: "Anuncio eliminado" updateAd: "Anuncio actualizado" + createAvatarDecoration: "Decoración de avatar creada" + updateAvatarDecoration: "Decoración de avatar actualizada" + deleteAvatarDecoration: "Decoración de avatar eliminada" + unsetUserAvatar: "Quitar decoración de avatar de este usuario" + unsetUserBanner: "Quitar banner de este usuario" _fileViewer: title: "Detalles del archivo" type: "Tipo de archivo" @@ -2144,3 +2300,57 @@ _fileViewer: uploadedAt: "Subido el" attachedNotes: "Notas adjuntas" thisPageCanBeSeenFromTheAuthor: "Esta página solo puede ser vista por el autor." +_externalResourceInstaller: + title: "Instalar desde sitio externo" + checkVendorBeforeInstall: "Asegúrate de que el distribuidor de este recurso es de confianza antes de proceder a la instalación." + _plugin: + title: "¿Quieres instalar este plugin?" + metaTitle: "Información del plugin" + _theme: + title: "¿Quieres instalar este tema?" + metaTitle: "Información del tema" + _meta: + base: "Esquema de color base" + _vendorInfo: + title: "Información del distribuidor" + endpoint: "Terminal referenciada" + hashVerify: "Verificación de hash" + _errors: + _invalidParams: + title: "Parámetros inválidos" + description: "No hay información suficiente para cargar datos de un sitio externo. Por favor, confirma la URL introducida." + _resourceTypeNotSupported: + title: "Este recurso externo no es compatible" + description: "El tipo de este recurso externo no es compatible. Por favor, contacta con el administrador del sitio." + _failedToFetch: + title: "No se pudo obtener los datos" + fetchErrorDescription: "Ha ocurrido un error al comunicarse con el sitio externo. Si no se soluciona tras intentarlo otra vez, por favor, contacta con el administrador del sitio." + parseErrorDescription: "Ha ocurrido un error al procesar los datos obtenidos del sitio externo. Por favor, contacta con el administrador del sitio." + _hashUnmatched: + title: "Verificación de datos fallida" + description: "Ha ocurrido un error al verificar la integridad de los datos obtenidos. Por seguridad, la instalación no se puede realizar. Por favor, contacta con el administrador del sitio." + _pluginParseFailed: + title: "Error de AiScript" + description: "Los datos se han obtenido correctamente, pero ha ocurrido un error de AiScript al procesarlos. Por favor, contacta con el autor del plugin. Se pueden ver más detalles del error en la consola de Javascript." + _pluginInstallFailed: + title: "Instalación del plugin fallida." + description: "Ha ocurrido un problema al instalar el plugin. Por favor, inténtalo de nuevo. Se pueden ver más detalles del error en la consola de Javascript." + _themeParseFailed: + title: "Análisis del tema fallido" + description: "Los datos se han obtenido correctamente, pero ha ocurrido un error al analizar el tema. Por favor, contacta con el autor. Se pueden ver más detalles del error en la consola de Javascript." + _themeInstallFailed: + title: "Instalación de tema fallida" + description: "Ha ocurrido un problema al instalar el tema. Por favor, inténtalo de nuevo. Se pueden ver más detalles del error en la consola de Javascript." +_dataSaver: + _media: + title: "Cargando Multimedia" + description: "Desactiva la carga automática de imágenes y vídeos. Tendrás que tocar en las imágenes y vídeos ocultos para cargarlos." + _avatar: + title: "Avatares animados" + description: "Desactiva la animación de los avatares. Las imágenes animadas pueden llegar a ser de mayor tamaño que las normales, por lo que al desactivarlas puedes reducir el consumo de datos." + _urlPreview: + title: "Vista previa de URLs" + description: "Desactiva la carga de vistas previas de las URLs." + _code: + title: "Resaltar código" + description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos." diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index c1d2d440d1..ac9e94a01a 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -75,7 +75,7 @@ import: "Importer" export: "Exporter" files: "Fichiers" download: "Télécharger" -driveFileDeleteConfirm: "Êtes-vous sûr de vouloir supprimer le fichier \"{name}\" ? Les notes liées à ce fichier seront aussi supprimées." +driveFileDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer le fichier « {name} » ? Les notes avec ce fichier joint seront aussi supprimées." unfollowConfirm: "Désirez-vous vous désabonner de {name} ?" exportRequested: "Vous avez demandé une exportation. L’opération pourrait prendre un peu de temps. Une fois terminée, le fichier sera ajouté au Drive." importRequested: "Vous avez initié un import. Cela pourrait prendre un peu de temps." @@ -121,7 +121,12 @@ sensitive: "Contenu sensible" add: "Ajouter" reaction: "Réactions" reactions: "Réactions" -reactionSetting: "Réactions à afficher dans le sélecteur de réactions" +emojiPicker: "Sélecteur d’émojis" +pinnedEmojisForReactionSettingDescription: "Vous pouvez définir les émojis épinglés lors de la réaction" +pinnedEmojisSettingDescription: "Vous pouvez définir les émojis épinglés lors de la saisie de l'émoji" +emojiPickerDisplay: "Affichage du sélecteur d'émojis" +overwriteFromPinnedEmojisForReaction: "Remplacer par les émojis épinglés pour la réaction" +overwriteFromPinnedEmojis: "Remplacer par les émojis épinglés globalement" reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter." rememberNoteVisibility: "Se souvenir de la visibilité des notes" attachCancel: "Supprimer le fichier attaché" @@ -157,6 +162,7 @@ addEmoji: "Ajouter un émoji" settingGuide: "Configuration proposée" cacheRemoteFiles: "Mise en cache des fichiers distants" cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis l’instance distante. La désactiver diminuera certes l’utilisation de l’espace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées." +youCanCleanRemoteFilesCache: "Vous pouvez supprimer tous les caches en cliquant le bouton 🗑️ dans la gestion des fichiers." cacheRemoteSensitiveFiles: "Mettre en cache les fichiers distants sensibles" cacheRemoteSensitiveFilesDescription: "Si vous désactivez ce paramètre, les fichiers sensibles distants ne seront pas mis en cache et un lien direct sera utilisé à la place" flagAsBot: "Ce compte est un robot" @@ -258,14 +264,15 @@ imageUrl: "URL de l’image" remove: "Supprimer" removed: "Supprimé" removeAreYouSure: "Êtes-vous sûr·e de vouloir supprimer « {x} » ?" -deleteAreYouSure: "Êtes-vous sûr·e de vouloir supprimer「{x}」?" +deleteAreYouSure: "Êtes-vous sûr·e de vouloir supprimer « {x} » ?" resetAreYouSure: "Voulez-vous réinitialiser ?" +areYouSure: "Êtes-vous sûr·e ?" saved: "Enregistré" messaging: "Discuter" upload: "Téléverser" keepOriginalUploading: "Garder l’image d’origine" keepOriginalUploadingDescription: "Conserve la version originale lors du téléchargement d'images. S'il est désactivé, le navigateur génère l'image pour la publication web lors du téléchargement." -fromDrive: "Depuis le Drive" +fromDrive: "Depuis le Disque" fromUrl: "Depuis une URL" uploadFromUrl: "Téléverser via une URL" uploadFromUrlDescription: "URL du fichier que vous souhaitez téléverser" @@ -299,7 +306,7 @@ dark: "Sombre" lightThemes: "Thèmes clairs" darkThemes: "Thèmes sombres" syncDeviceDarkMode: "Utiliser le mode sombre de votre appareil" -drive: "Drive" +drive: "Disque" fileName: "Nom du fichier" selectFile: "Choisir le fichier" selectFiles: "Choisir les fichiers" @@ -310,8 +317,9 @@ folderName: "Nom du dossier" createFolder: "Créer un dossier" renameFolder: "Renommer le dossier" deleteFolder: "Supprimer le dossier" +folder: "Dossier" addFile: "Ajouter un fichier" -emptyDrive: "Le Drive est vide" +emptyDrive: "Le Disque est vide" emptyFolder: "Le dossier est vide" unableToDelete: "Suppression impossible" inputNewFileName: "Entrez un nouveau nom de fichier" @@ -355,8 +363,8 @@ disablingTimelinesInfo: "Même si vous désactivez ces fils, les administrateur registration: "S’inscrire" enableRegistration: "Autoriser les nouvelles inscriptions" invite: "Inviter" -driveCapacityPerLocalAccount: "Volume du Drive par utilisateur local" -driveCapacityPerRemoteAccount: "Volume du Drive par utilisateur distant" +driveCapacityPerLocalAccount: "Capacité de stockage du Disque par utilisateur local" +driveCapacityPerRemoteAccount: "Capacité de stockage du Disque par utilisateur distant" inMb: "en mégaoctets" bannerUrl: "URL de l’image de la bannière" backgroundImageUrl: "URL de l'image d'arrière-plan" @@ -428,6 +436,7 @@ lastUsed: "Dernier utilisé" lastUsedAt: "Dernière utilisation : {t}" unregister: "Se désinscrire" passwordLessLogin: "Se connecter sans mot de passe" +passwordLessLoginDescription: "Se connecter uniquement avec une clé de sécurité ou une clé d'accès sans utiliser de mot de passe" resetPassword: "Réinitialiser le mot de passe" newPasswordIs: "Votre nouveau mot de passe est \"{password}\"" reduceUiAnimation: "Réduire les animations dans l’interface" @@ -435,7 +444,6 @@ share: "Partager" notFound: "Non trouvé" notFoundDescription: "Aucune page ne correspond à l’URL spécifiée." uploadFolder: "Emplacement de téléversement par défaut" -cacheClear: "Vider le cache" markAsReadAllNotifications: "Marquer toutes les notifications comme lues" markAsReadAllUnreadNotes: "Marquer toutes les notes comme lues" markAsReadAllTalkMessages: "Marquer toutes les discussions comme lues" @@ -483,6 +491,7 @@ showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol" noHistory: "Pas d'historique" signinHistory: "Historique de connexion" enableAdvancedMfm: "Activer la MFM avancée" +enableAnimatedMfm: "Activer le MFM animé" doing: "En cours..." category: "Catégorie" tags: "Étiquettes" @@ -491,6 +500,7 @@ createAccount: "Créer un compte" existingAccount: "Compte existant" regenerate: "Générer à nouveau" fontSize: "Taille de la police" +mediaListWithOneImageAppearance: "Hauteur des listes de médias n'ayant qu'une image " limitTo: "Limiter à {x}" noFollowRequests: "Vous n’avez aucune demande d’abonnement en attente" openImageInNewTab: "Ouvrir les images dans un nouvel onglet" @@ -528,6 +538,7 @@ objectStorageSetPublicRead: "Régler sur « public » lors de l'envoi" serverLogs: "Journal du serveur" deleteAll: "Supprimer tout" showFixedPostForm: "Afficher le formulaire de publication en haut du fil d'actualité" +showFixedPostFormInChannel: "Afficher le formulaire de publication en haut du fil (canaux)" withRepliesByDefaultForNewlyFollowed: "Afficher les réponses des nouvelles personnes que vous suivez dans le fil par défaut" newNoteRecived: "Voir les nouvelles notes" sounds: "Sons" @@ -538,6 +549,8 @@ showInPage: "Afficher dans la page" popout: "Fenêtre contextuelle" volume: "Volume" masterVolume: "Volume principal" +notUseSound: "Ne pas émettre de son" +useSoundOnlyWhenActive: "Émettre des sons uniquement quand Misskey est active" details: "Détails" chooseEmoji: "Choisissez un émoji" unableToProcess: "L’opération n’a pas pu être complétée." @@ -558,9 +571,13 @@ output: "Sortie" script: "Script" disablePagesScript: "Désactiver AiScript sur les Pages" updateRemoteUser: "Mettre à jour les informations de l’utilisateur·rice distant·e" +unsetUserAvatar: "Supprimer l’avatar" +unsetUserAvatarConfirm: "Êtes-vous sûr·e de vouloir supprimer l'avatar ?" +unsetUserBanner: "Supprimer la bannière" +unsetUserBannerConfirm: "Êtes-vous sûr·e de vouloir supprimer la bannière ?" deleteAllFiles: "Supprimer tous les fichiers" deleteAllFilesConfirm: "Êtes-vous sûr·e de vouloir supprimer tous les fichiers ?" -removeAllFollowing: "Retenir tous les abonnements" +removeAllFollowing: "Se désabonner de tous les utilisateurs auxquels vous êtes abonné·e" removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Veuillez lancer cette action dans les cas où l’instance n’existe plus, etc." userSuspended: "Cet·te utilisateur·rice a été suspendu·e." userSilenced: "Cette utilisateur·trice a été mis·e en sourdine." @@ -629,6 +646,7 @@ smtpSecure: "Utiliser SSL/TLS implicitement dans les connexions SMTP" smtpSecureInfo: "Désactiver cette option lorsque STARTTLS est utilisé" testEmail: "Tester la distribution de courriel" wordMute: "Filtre de mots" +hardWordMute: "Filtre de mots dur" regexpError: "Erreur d’expression régulière" regexpErrorDescription: "Une erreur s'est produite dans l'expression régulière sur la ligne {ligne} de votre mot muet {tab} :" instanceMute: "Instance en sourdine" @@ -701,14 +719,15 @@ pollVotesCount: "Nombre de votes envoyés" pollVotedCount: "Nombre de votes reçus" yes: "Oui" no: "Non" -driveFilesCount: "Nombre de fichiers dans le Drive" -driveUsage: "Utilisation du Drive" +driveFilesCount: "Nombre de fichiers sur le Disque" +driveUsage: "Utilisation du Disque" noCrawle: "Refuser l'indexation par les robots" noCrawleDescription: "Demandez aux moteurs de recherche de ne pas indexer votre page de profil, vos notes, vos pages, etc." lockedAccountInfo: "À moins que vous ne définissiez la visibilité de votre note sur \"Abonné-e-s\", vos notes sont visibles par tous, même si vous exigez que les demandes d'abonnement soient approuvées manuellement." alwaysMarkSensitive: "Marquer les médias comme contenu sensible par défaut" loadRawImages: "Affichage complet des images jointes au lieu des vignettes" disableShowingAnimatedImages: "Désactiver l'animation des images" +highlightSensitiveMedia: "Mettre en évidence les médias sensibles" verificationEmailSent: "Un e-mail de vérification a été envoyé. Veuillez accéder au lien pour compléter la vérification." notSet: "Non défini" emailVerified: "Votre adresse e-mail a été vérifiée." @@ -845,7 +864,7 @@ pubSub: "Comptes Pub/Sub" lastCommunication: "Dernière communication" resolved: "Résolu" unresolved: "En attente" -breakFollow: "Ne plus suivre" +breakFollow: "Supprimer l'abonné·e" breakFollowConfirm: "Êtes-vous sûr de vouloir vous désabonner ?" itsOn: "Activé" itsOff: "Désactivé" @@ -861,8 +880,8 @@ makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions d classic: "Classique" muteThread: "Masquer cette discussion" unmuteThread: "Ne plus masquer le fil" -ffVisibility: "Visibilité des abonnés/abonnements" -ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent." +followingVisibility: "Visibilité des abonnements" +followersVisibility: "Visibilité des abonnés" continueThread: "Afficher la suite du fil" deleteAccountConfirm: "Votre compte sera supprimé. Êtes vous certain ?" incorrectPassword: "Le mot de passe est incorrect." @@ -904,7 +923,7 @@ noEmailServerWarning: "Serveur de courrier non configuré." thereIsUnresolvedAbuseReportWarning: "Il n’y a aucun rapport non résolu." recommended: "Recommandé" check: "Vérifier" -driveCapOverrideLabel: "Modifier la capacité de stockage du drive de cet·te utilisateur·rice" +driveCapOverrideLabel: "Modifier la capacité de stockage du Disque de cet·te utilisateur·rice" driveCapOverrideCaption: "Si une valeur inférieure à 0 est spécifiée, elle est annulée." requireAdminForView: "Vous devez être connecté avec un compte administrateur pour les visualiser." isSystemAccount: "Ces comptes sont automatiquement créés et gérés par le système." @@ -962,6 +981,7 @@ show: "Affichage" neverShow: "Ne plus afficher" remindMeLater: "Peut-être plus tard" didYouLikeMisskey: "Avez-vous aimé Misskey ?" +pleaseDonate: "Misskey est le logiciel libre utilisé par {host}. Merci de faire un don pour que nous puissions continuer à le développer !" roles: "Rôles" role: "Rôles" noRole: "Aucun rôle" @@ -974,12 +994,15 @@ manageCustomEmojis: "Gestion des émojis personnalisés" manageAvatarDecorations: "Gérer les décorations d'avatar" youCannotCreateAnymore: "Vous avez atteint la limite de création." cannotPerformTemporary: "Temporairement indisponible" +cannotPerformTemporaryDescription: "Temporairement indisponible puisque le nombre d'opérations dépasse la limite. Veuillez patienter un peu, puis réessayer." invalidParamError: "Paramètres invalides" permissionDeniedError: "Opération refusée" +permissionDeniedErrorDescription: "Ce compte n'a pas la permission d'effectuer cette opération." preset: "Préréglage" selectFromPresets: "Sélectionner à partir des préréglages" achievements: "Accomplissements" gotInvalidResponseError: "Réponse du serveur invalide" +gotInvalidResponseErrorDescription: "Il se peut que le serveur soit hors ligne ou en maintenance. Veuillez réessayer plus tard." thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes." thisPostMayBeAnnoyingHome: "Publier vers le fil principal" thisPostMayBeAnnoyingCancel: "Annuler" @@ -989,16 +1012,34 @@ internalServerError: "Erreur interne du serveur" copyErrorInfo: "Copier les détails de l’erreur" joinThisServer: "S'inscrire à cette instance" exploreOtherServers: "Trouver une autre instance" +letsLookAtTimeline: "Jetez un coup d'œil au fil" +disableFederationConfirm: "Voulez-vous vraiment désactiver la fédération ?" +disableFederationConfirmWarn: "Même sans fédération, la note ne sera pas privée. Dans la plupart des cas, ce n'est pas nécessaire de désactiver la fédération." disableFederationOk: "Désactiver" +invitationRequiredToRegister: "Actuellement, cette instance est uniquement sur invitation. Seuls ceux qui ont un code d'invitation peuvent s'inscrire." +emailNotSupported: "Cette instance ne prend pas en charge l'envoi de courriels" postToTheChannel: "Publier au canal" +cannotBeChangedLater: "Cela ne peut pas être modifié plus tard." +reactionAcceptance: "Acceptation des réactions" likeOnly: "Les favoris uniquement" +likeOnlyForRemote: "Toutes (mentions j'aime seulement pour les instances distantes)" +nonSensitiveOnly: "Non sensibles seulement" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non sensibles seulement (mentions j'aime seulement pour les instances distantes)" +rolesAssignedToMe: "Rôles attribués à moi" +resetPasswordConfirm: "Souhaitez-vous réinitialiser votre mot de passe ?" sensitiveWords: "Mots sensibles" +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." notesSearchNotAvailable: "La recherche de notes n'est pas disponible." license: "Licence" myClips: "Mes clips" +drivecleaner: "Nettoyeur du Disque" retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur." +enableChartsForRemoteUser: "Générer les graphiques pour les utilisateurs distants" +enableChartsForFederatedInstances: "Générer les graphiques pour les instances distantes" showClipButtonInNoteFooter: "Ajouter « Clip » au menu d'action de la note" reactionsDisplaySize: "Taille de l'affichage des réactions" +limitWidthOfReaction: "Limiter la largeur maximale des réactions et les afficher en taille réduite" noteIdOrUrl: "Identifiant de la note ou URL" video: "Vidéo" videos: "Vidéos" @@ -1009,6 +1050,7 @@ accountMovedShort: "Ce compte a migré" operationForbidden: "Opération non autorisée" forceShowAds: "Toujours afficher les publicités" addMemo: "Ajouter un mémo" +editMemo: "Éditer le mémo" reactionsList: "Réactions" renotesList: "Liste de renotes" notificationDisplay: "Style des notifications" @@ -1021,10 +1063,13 @@ vertical: "Vertical" horizontal: "Latéral" position: "Position" serverRules: "Règles du serveur" +pleaseConfirmBelowBeforeSignup: "Pour vous inscrire sur cette instance, vous devez confirmer et accepter le contenu suivant." pleaseAgreeAllToContinue: "Pour continuer, veuillez accepter tous les champs ci-dessus." continue: "Continuer" preservedUsernames: "Noms d'utilisateur·rice réservés" +createNoteFromTheFile: "Rédiger une note de ce fichier" archive: "Archive" +channelArchiveConfirmTitle: "Voulez-vous vraiment archiver {name} ?" thisChannelArchived: "Ce canal a été archivé." displayOfNote: "Affichage de la note" initialAccountSetting: "Configuration initiale du profil" @@ -1035,11 +1080,15 @@ options: "Options" specifyUser: "Spécifier l'utilisateur·rice" failedToPreviewUrl: "Aperçu d'URL échoué" update: "Mettre à jour" +rolesThatCanBeUsedThisEmojiAsReaction: "Rôles qui peuvent utiliser cet émoji comme réaction" later: "Plus tard" goToMisskey: "Retour vers Misskey" +additionalEmojiDictionary: "Dictionnaires d'émojis additionnels" installed: "Installé" +branding: "Image de marque" expirationDate: "Date d’expiration" waitingForMailAuth: "En attente de la vérification de l'adresse courriel" +inviteCodeCreator: "Créateur·rice de ce code d'invitation" usedAt: "Utilisé le" unused: "Non-utilisé" used: "Utilisé" @@ -1080,6 +1129,7 @@ tosAndPrivacyPolicy: "Conditions d'utilisation et politique de confidentialité" avatarDecorations: "Décorations d'avatar" attach: "Mettre" detach: "Enlever" +detachAll: "Tout enlever" angle: "Angle" flip: "Inverser" showAvatarDecorations: "Afficher les décorations d'avatar" @@ -1091,6 +1141,12 @@ useGroupedNotifications: "Grouper les notifications" signupPendingError: "Un problème est survenu lors de la vérification de votre adresse e-mail. Le lien a peut-être expiré." cwNotationRequired: "Si « Masquer le contenu » est activé, une description doit être fournie." doReaction: "Réagir" +code: "Code" +reloadRequiredToApplySettings: "Le rafraîchissement est nécessaire pour que les paramètres prennent effet." +remainingN: "Restants : {n}" +overwriteContentConfirm: "Voulez-vous remplacer le contenu actuel ?" +seasonalScreenEffect: "Effet d'écran saisonnier" +decorate: "Décorer" _announcement: readConfirmTitle: "Marquer comme lu ?" shouldNotBeUsedToPresentPermanentInfo: "Puisque cela pourrait nuire considérablement à l'expérience utilisateur pour les nouveaux utilisateurs, il est recommandé d'utiliser les annonces pour afficher des informations temporaires plutôt que des informations persistantes." @@ -1160,7 +1216,7 @@ _initialTutorial: tryThisFile: "Essayez de marquer l'image jointe à ce formulaire de publication comme sensible !" _exampleNote: note: "Oups, j'ai échoué à ouvrir le couvercle du natto..." - method: "Pour marquer un fichier joint comme sensible, cliquez sur la vignette du fichier, ouvrez le menu et cliquez sur « marquer comme sensible » ." + method: "Pour marquer un fichier joint comme sensible, cliquez sur la vignette du fichier pour ouvrir le menu et cliquez sur « marquer comme sensible » ." sensitiveSucceeded: "Quand vous joignez des fichiers, veuillez indiquer la sensibilité selon les règles du serveur." doItToContinue: "Marquez le fichier joint comme sensible pour procéder." _done: @@ -1324,6 +1380,8 @@ _role: description: "Description du rôle" permission: "Rôle et autorisations" assignTarget: "Attribuer" + manualRoles: "Rôles manuels" + conditionalRoles: "Rôles conditionnels" condition: "Condition" isPublic: "Rôle public" options: "Options" @@ -1341,8 +1399,10 @@ _role: _options: canManageCustomEmojis: "Gestion des émojis personnalisés" canManageAvatarDecorations: "Gestion des décorations d'avatar" + driveCapacity: "Capacité de stockage du Disque" wordMuteMax: "Nombre maximal de caractères dans le filtre de mots" canUseTranslator: "Usage de la fonctionnalité de traduction" + avatarDecorationLimit: "Nombre maximal de décorations d'avatar" _sensitiveMediaDetection: description: "L'apprentissage automatique peut être utilisé pour détecter automatiquement les médias sensibles à modérer. La sollicitation des serveurs augmente légèrement." sensitivity: "Sensibilité de la détection" @@ -1410,7 +1470,7 @@ _preferencesBackups: nameAlreadyExists: "Le nom de sauvegarde \"{name}\" existe déjà. Veuillez spécifier un autre nom." applyConfirm: "Voulez-vous appliquer la sauvegarde '{name}' au dispositif actuel ? La configuration actuelle de l'appareil sera perdue." saveConfirm: "Voulez-vous écraser {name} ?" - deleteConfirm: "Voulez-vous supprimer {name} ?" + deleteConfirm: "Êtes-vous sûr·e de vouloir supprimer {name} ?" renameConfirm: "Voulez-vous remplacer \"{old}\" par \"{new}\" ?" noBackups: "Aucune sauvegarde n'est disponible. L'option \"Nouvelle sauvegarde\" vous permet de sauvegarder la configuration actuelle du client sur le serveur." createdAt: "Créé : {date} {time}" @@ -1547,6 +1607,14 @@ _sfx: notification: "Notifications" antenna: "Réception de l’antenne" channel: "Notifications de canal" + reaction: "Lors de la sélection de la réaction" +_soundSettings: + driveFile: "Utiliser un effet sonore sur le Disque" + driveFileWarn: "Veuillez sélectionner le fichier sur le Disque" + driveFileTypeWarn: "Ce fichier n'est pas pris en charge" + driveFileTypeWarnDescription: "Veuillez sélectionner un fichier audio" + driveFileDurationWarn: "L'effet sonore est trop long" + driveFileDurationWarnDescription: "Utiliser un effet sonore long peut affecter l'utilisation de Misskey. Voulez-vous encore continuer ?" _ago: future: "Futur" justNow: "à l’instant" @@ -1558,6 +1626,14 @@ _ago: monthsAgo: "Il y a {n} mois" yearsAgo: "Il y a {n} ans" invalid: "Il n'y a rien à voir ici" +_timeIn: + seconds: "Dans {n}s" + minutes: "Dans {n}min" + hours: "Dans {n}h" + days: "Dans {n}j" + weeks: "Dans {n} sem." + months: "Dans {n} mois" + years: "Dans {n}a" _time: second: "s" minute: "min" @@ -1575,7 +1651,7 @@ _2fa: securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion grâce à une clé de sécurité matérielle qui prend en charge FIDO2, ou bien en configurant l'authentification par empreinte digitale ou par code PIN sur votre appareil." securityKeyName: "Nom de la clé" removeKey: "Supprimer la clé de sécurité" - removeKeyConfirm: "Voulez-vous supprimer {name} ?" + removeKeyConfirm: "Êtes-vous sûr·e de vouloir supprimer {name} ?" renewTOTPOk: "Reconfigurer" renewTOTPCancel: "Pas maintenant" backupCodes: "Codes de Secours" @@ -1584,8 +1660,8 @@ _permissions: "write:account": "Mettre à jour les informations de votre compte" "read:blocks": "Voir les comptes bloqués" "write:blocks": "Gérer les comptes bloqués" - "read:drive": "Parcourir le Drive" - "write:drive": "Écrire sur le Drive" + "read:drive": "Parcourir le Disque" + "write:drive": "Modifier le Disque" "read:favorites": "Afficher les favoris" "write:favorites": "Gérer les favoris" "read:following": "Voir les informations de vos abonnements" @@ -1605,7 +1681,7 @@ _permissions: "read:page-likes": "Voir les mentions « J'aime » des pages" "write:page-likes": "Gérer les mentions « J'aime » sur les pages" "read:user-groups": "Voir les groupes d'utilisateur·rice·s" - "write:user-groups": "Éditer les groupes des utilisateur·rice·s" + "write:user-groups": "Éditer les groupes d'utilisateur·rice·s" "read:channels": "Lire les canaux" "write:channels": "Gérer les canaux" "read:gallery": "Voir la galerie" @@ -1659,6 +1735,7 @@ _widgets: userList: "Liste utilisateur" _userList: chooseList: "Sélectionner une liste" + birthdayFollowings: "Utilisateurs qui fêtent l'anniversaire aujourd'hui" _cw: hide: "Masquer" show: "Afficher le contenu" @@ -1695,6 +1772,7 @@ _visibility: followersDescription: "Publier à vos abonné·e·s uniquement" specified: "Direct" specifiedDescription: "Publier uniquement aux utilisateur·rice·s mentionné·e·s" + disableFederation: "Défédérer" _postForm: replyPlaceholder: "Répondre à cette note ..." quotePlaceholder: "Citez cette note ..." @@ -1716,8 +1794,9 @@ _profile: metadataDescription: "Vous pouvez afficher jusqu'à quatre informations supplémentaires dans votre profil." metadataLabel: "Étiquette" metadataContent: "Contenu" - changeAvatar: "Changer l'image de profil" + changeAvatar: "Changer l'avatar" changeBanner: "Changer de bannière" + avatarDecorationMax: "Vous pouvez mettre au plus {max} décorations d'avatar." _exportOrImport: allNotes: "Toutes les notes" followingList: "Abonnements" @@ -1828,6 +1907,7 @@ _notification: yourFollowRequestAccepted: "Votre demande d’abonnement a été accepté" pollEnded: "Les résultats du sondage sont disponibles" unreadAntennaNote: "Antenne {name}" + roleAssigned: "Rôle attribué" emptyPushNotificationMessage: "Les notifications push ont été mises à jour" achievementEarned: "Accomplissement" testNotification: "Tester la notification" @@ -1845,6 +1925,7 @@ _notification: pollEnded: "Sondages se cloturant" receiveFollowRequest: "Demande d'abonnement reçue" followRequestAccepted: "Demande d'abonnement acceptée" + roleAssigned: "Rôle reçu" achievementEarned: "Accomplissement" app: "Notifications provenant des apps" _actions: @@ -1878,6 +1959,9 @@ _deck: channel: "Canal" mentions: "Mentions" direct: "Direct" +_drivecleaner: + orderBySizeDesc: "Taille descendante" + orderByCreatedAtAsc: "Date d'ajout ascendante" _webhookSettings: name: "Nom" active: "Activé" @@ -1915,6 +1999,8 @@ _moderationLogTypes: createAvatarDecoration: "Décoration d'avatar créée" updateAvatarDecoration: "Décoration d'avatar mise à jour" deleteAvatarDecoration: "Décoration d'avatar supprimée" + unsetUserAvatar: "Supprimer l'avatar de l'utilisateur·rice" + unsetUserBanner: "Supprimer la bannière de l'utilisateur·rice" _fileViewer: title: "Détails du fichier" type: "Type du fichier" @@ -1964,3 +2050,16 @@ _externalResourceInstaller: _themeInstallFailed: title: "Échec d'installation du thème" description: "Il y a eu un problème lors de l'installation du thème. Veuillez réessayer. Pour plus de détails sur l'erreur, veuillez consulter la console JavaScript." +_dataSaver: + _media: + title: "Chargement des médias" + description: "Empêche le chargement automatique des images et des vidéos. Appuyez sur les images et les vidéos cachées pour les charger." + _avatar: + title: "Animation d'avatars" + description: "Arrête l'animation d'avatars. Comme les images animées peuvent être plus volumineuses que les images normales, cela permet de réduire davantage le trafic de données." + _urlPreview: + title: "Vignettes d'aperçu des URL" + description: "Les vignettes d'aperçu des URL ne seront plus chargées." + _code: + title: "Mise en évidence du code" + description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données." diff --git a/locales/generateDTS.js b/locales/generateDTS.js index 7af773f3b1..d3afdd6e15 100644 --- a/locales/generateDTS.js +++ b/locales/generateDTS.js @@ -56,6 +56,18 @@ export default function generateDTS() { ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, ), ), + ts.factory.createFunctionDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + undefined, + ts.factory.createIdentifier('build'), + undefined, + [], + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier('Locale'), + undefined, + ), + undefined, + ), ts.factory.createExportDefault(ts.factory.createIdentifier('locales')), ]; const printed = ts.createPrinter({ diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 8e516bc0fc..00844550fd 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -121,7 +121,10 @@ sensitive: "Konten sensitif" add: "Tambahkan" reaction: "Reaksi" reactions: "Reaksi" -reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi" +emojiPicker: "Emoji Picker" +pinnedEmojisForReactionSettingDescription: "Atur sematan emoji pada reaksi" +pinnedEmojisSettingDescription: "Atur sematan emoji pada masukan emoji" +emojiPickerDisplay: "Tampilan Emoji Picker" reactionSettingDescription2: "Geser untuk memindah urutan emoji, klik untuk menghapus, tekan \"+\" untuk menambahkan" rememberNoteVisibility: "Ingat pengaturan visibilitas catatan" attachCancel: "Hapus lampiran" @@ -261,6 +264,7 @@ removed: "Telah dihapus" removeAreYouSure: "Apakah kamu yakin ingin menghapus \"{x}\"?" deleteAreYouSure: "Apakah kamu yakin ingin menghapus \"{x}\"?" resetAreYouSure: "Yakin mau atur ulang?" +areYouSure: "Apakah kamu yakin?" saved: "Telah disimpan" messaging: "Pesan" upload: "Unggah" @@ -311,6 +315,7 @@ folderName: "Nama folder" createFolder: "Buat folder" renameFolder: "Ubah nama folder" deleteFolder: "Hapus folder" +folder: "Folder" addFile: "Tambahkan berkas" emptyDrive: "Drive kosong" emptyFolder: "Folder kosong" @@ -437,7 +442,6 @@ share: "Bagikan" notFound: "Tidak dapat ditemukan" notFoundDescription: "Tidak ada halaman sesuai dengan URL yang ditentukan." uploadFolder: "Lokasi unggah folder bawaan" -cacheClear: "Bersihkan tembolok" markAsReadAllNotifications: "Tandai semua notifikasi telah dibaca" markAsReadAllUnreadNotes: "Tandai semua catatan telah dibaca" markAsReadAllTalkMessages: "Tandai semua pesan telah dibaca" @@ -544,6 +548,8 @@ showInPage: "Tampilkan di halaman" popout: "Pop-out" volume: "Volume" masterVolume: "Master volume" +notUseSound: "Tidak ada keluaran suara" +useSoundOnlyWhenActive: "Hanya keluarkan suara jika Misskey sedang aktif" details: "Selengkapnya" chooseEmoji: "Pilih emoji" unableToProcess: "Operasi tersebut tidak dapat diselesaikan." @@ -564,6 +570,10 @@ output: "Keluaran" script: "Script" disablePagesScript: "Nonaktifkan script pada halaman" updateRemoteUser: "Perbaharui informasi pengguna instansi luar" +unsetUserAvatar: "Hapus avatar" +unsetUserAvatarConfirm: "Apakah kamu yakin ingin menghapus avatar?" +unsetUserBanner: "Hapus banner" +unsetUserBannerConfirm: "Apakah kamu yakin ingin menghapus banner?" deleteAllFiles: "Hapus semua berkas" deleteAllFilesConfirm: "Apakah kamu yakin ingin menghapus semua berkas?" removeAllFollowing: "Batalkan mengikuti semua pengguna" @@ -635,6 +645,7 @@ smtpSecure: "Gunakan SSL/TLS implisit untuk koneksi SMTP" smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS" testEmail: "Tes pengiriman surel" wordMute: "Bisukan kata" +hardWordMute: "Pembisuan kata keras" regexpError: "Kesalahan ekspresi reguler" regexpErrorDescription: "Galat terjadi pada baris {line} ekspresi reguler dari {tab} kata yang dibisukan:" instanceMute: "Bisukan instansi" @@ -868,8 +879,6 @@ makeReactionsPublicDescription: "Pengaturan ini akan membuat daftar dari semua r classic: "Klasik" muteThread: "Bisukan thread" unmuteThread: "Suarakan thread" -ffVisibility: "Visibilitas Mengikuti/Pengikut" -ffVisibilityDescription: "Mengatur siapa yang dapat melihat pengikutmu dan yang kamu ikuti." continueThread: "Lihat lanjutan thread" deleteAccountConfirm: "Akun akan dihapus. Apakah kamu yakin?" incorrectPassword: "Kata sandi salah." @@ -979,6 +988,7 @@ assign: "Tetapkan\n" unassign: "Batalkan penetapan" color: "Warna" manageCustomEmojis: "Kelola Emoji Kustom" +manageAvatarDecorations: "Kelola dekorasi avatar" youCannotCreateAnymore: "Kamu melewati batas pembuatan." cannotPerformTemporary: "Sementara Tidak Tersedia" cannotPerformTemporaryDescription: "Aksi ini tidak dapat dilakukan sementara karena melewati batas eksekusi. Mohon tunggu sejenak dan coba lagi." @@ -1019,6 +1029,8 @@ resetPasswordConfirm: "Yakin untuk mereset kata sandimu?" 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." sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." +hiddenTags: "Tagar tersembunyi" +hiddenTagsDescription: "Pilih tanda yang mana akan tidak diperlihatkan dalam daftar tren.\nTanda lebih dari satu dapat didaftarkan dengan tiap baris." notesSearchNotAvailable: "Pencarian catatan tidak tersedia." license: "Lisensi" unfavoriteConfirm: "Yakin ingin menghapusnya dari favorit?" @@ -1031,6 +1043,7 @@ enableChartsForRemoteUser: "Buat bagan data pengguna instansi luar" enableChartsForFederatedInstances: "Buat bagan data peladen instansi luar" showClipButtonInNoteFooter: "Tambahkan \"Klip\" ke menu aksi catatan" reactionsDisplaySize: "Ukuran tampilan reaksi" +limitWidthOfReaction: "Batasi lebar maksimum reaksi dan tampilkan dalam ukuran terbatasi." noteIdOrUrl: "ID catatan atau URL" video: "Video" videos: "Video" @@ -1132,6 +1145,10 @@ mutualFollow: "Saling mengikuti" fileAttachedOnly: "Hanya catatan dengan berkas" showRepliesToOthersInTimeline: "Tampilkan balasan ke pengguna lain dalam lini masa" hideRepliesToOthersInTimeline: "Sembunyikan balasan ke orang lain dari lini masa" +showRepliesToOthersInTimelineAll: "Tampilkan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa" +hideRepliesToOthersInTimelineAll: "Sembuyikan 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?" externalServices: "Layanan eksternal" impressum: "Impressum" impressumUrl: "Tautan Impressum" @@ -1139,7 +1156,25 @@ impressumDescription: "Pada beberapa negara seperti Jerman, inklusi dari informa privacyPolicy: "Kebijakan Privasi" privacyPolicyUrl: "Tautan Kebijakan Privasi" tosAndPrivacyPolicy: "Syarat dan Ketentuan serta Kebijakan Privasi" +avatarDecorations: "Dekorasi avatar" +attach: "Lampirkan" +detach: "Hapus" +detachAll: "Lepas Semua" +angle: "Sudut" flip: "Balik" +showAvatarDecorations: "Tampilkan dekorasi avatar" +releaseToRefresh: "Lepaskan untuk memuat ulang" +refreshing: "Sedang memuat ulang..." +pullDownToRefresh: "Tarik ke bawah untuk memuat ulang" +disableStreamingTimeline: "Nonaktifkan pembaharuan lini masa real-time" +useGroupedNotifications: "Tampilkan notifikasi secara dikelompokkan" +signupPendingError: "Terdapat masalah ketika memverifikasi alamat surel. Tautan kemungkinan telah kedaluwarsa." +cwNotationRequired: "Jika \"Sembunyikan konten\" diaktifkan, deskripsi harus disediakan." +doReaction: "Tambahkan reaksi" +code: "Kode" +reloadRequiredToApplySettings: "Muat ulang diperlukan untuk menerapkan pengaturan." +remainingN: "Sisa : {n}" +decorate: "Dekor" _announcement: 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." @@ -1149,6 +1184,7 @@ _announcement: tooManyActiveAnnouncementDescription: "Terlalu banyak pengumuman dapat memperburuk pengalaman pengguna. Mohon pertimbangkan untuk mengarsipkan pengumuman yang sudah usang/tidak relevan." readConfirmTitle: "Tandai telah dibaca?" readConfirmText: "Aksi ini akan menandai konten dari \"{title}\" telah dibaca." + silence: "Tiada notifikasi" _initialAccountSetting: accountCreated: "Akun kamu telah sukses dibuat!" letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu." @@ -1161,8 +1197,37 @@ _initialAccountSetting: pushNotificationDescription: "Menyalakan notifikasi dorong akan membuatmu menerima notifikasi dari {name} secara langsung ke perangkatmu." initialAccountSettingCompleted: "Pengaturan profil selesai!" haveFun: "Selamat menikmati, {name}!" + startTutorial: "Mulai Tutorial" skipAreYouSure: "Yakin melewati atur profil?" laterAreYouSure: "Yakin banget untuk atur profil nanti?" +_initialTutorial: + launchTutorial: "Lihat Tutorial" + title: "Tutorial" + wellDone: "Kerja bagus!" + skipAreYouSure: "Berhenti dari Tutorial?" + _landing: + title: "Selamat datang di Tutorial" + description: "Di sini kamu dapat mempelajari dasar-dasar dari penggunaan Misskey dan fitur-fiturnya." + _note: + title: "Apa itu Catatan?" + _reaction: + title: "Apa itu Reaksi?" + _timeline: + title: "Konsep Lini Masa" + _postNote: + title: "Pengaturan posting Catatan" + _visibility: + public: "Perlihatkan catatan ke semua pengguna." + home: "Hanya publik ke lini masa Beranda. Pengguna yang mengunjungi profilmu melalui pengikut dan renote dapat melihatnya." + followers: "Perlihatkan ke pengikut saja. Hanya pengikut yang dapat melihat postinganmu dan tidak dapat direnote oleh siapapun." + direct: "Hanya perlihatkan ke pengguna spesifik dan penerima akan diberi tahu. Dapat juga digunakan sebagai alternatif dari pesan langsung." + _cw: + title: "Peringatan Konten (CW)" + _exampleNote: + cw: "Peringatan: Bikin Lapar!" + note: "Baru aja makan donat berlapis coklat 🍩😋" + _howToMakeAttachmentsSensitive: + title: "Bagaimana menandai lampiran sebagai sensitif?" _serverRules: description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan." _serverSettings: @@ -1474,6 +1539,7 @@ _role: inviteLimitCycle: "Interval Penerbitan Kode Undangan" inviteExpirationTime: "Interval kedaluwarsa undangan" canManageCustomEmojis: "Dapat mengelola Emoji kustom" + canManageAvatarDecorations: "Kelola dekorasi avatar" driveCapacity: "Kapasitas Drive" alwaysMarkNsfw: "Selalu tandai berkas sebagai NSFW" pinMax: "Jumlah maksimal catatan yang disematkan" diff --git a/locales/index.d.ts b/locales/index.d.ts index 9aeaeb9e56..dd2f34a69a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -128,7 +128,12 @@ export interface Locale { "add": string; "reaction": string; "reactions": string; - "reactionSetting": string; + "emojiPicker": string; + "pinnedEmojisForReactionSettingDescription": string; + "pinnedEmojisSettingDescription": string; + "emojiPickerDisplay": string; + "overwriteFromPinnedEmojisForReaction": string; + "overwriteFromPinnedEmojis": string; "reactionSettingDescription2": string; "rememberNoteVisibility": string; "attachCancel": string; @@ -274,6 +279,7 @@ export interface Locale { "removeAreYouSure": string; "deleteAreYouSure": string; "resetAreYouSure": string; + "areYouSure": string; "saved": string; "messaging": string; "upload": string; @@ -324,6 +330,7 @@ export interface Locale { "createFolder": string; "renameFolder": string; "deleteFolder": string; + "folder": string; "addFile": string; "emptyDrive": string; "emptyFolder": string; @@ -451,7 +458,6 @@ export interface Locale { "notFound": string; "notFoundDescription": string; "uploadFolder": string; - "cacheClear": string; "markAsReadAllNotifications": string; "markAsReadAllUnreadNotes": string; "markAsReadAllTalkMessages": string; @@ -561,6 +567,8 @@ export interface Locale { "popout": string; "volume": string; "masterVolume": string; + "notUseSound": string; + "useSoundOnlyWhenActive": string; "details": string; "chooseEmoji": string; "unableToProcess": string; @@ -581,6 +589,10 @@ export interface Locale { "script": string; "disablePagesScript": string; "updateRemoteUser": string; + "unsetUserAvatar": string; + "unsetUserAvatarConfirm": string; + "unsetUserBanner": string; + "unsetUserBannerConfirm": string; "deleteAllFiles": string; "deleteAllFilesConfirm": string; "removeAllFollowing": string; @@ -652,6 +664,7 @@ export interface Locale { "smtpSecureInfo": string; "testEmail": string; "wordMute": string; + "hardWordMute": string; "regexpError": string; "regexpErrorDescription": string; "instanceMute": string; @@ -888,8 +901,8 @@ export interface Locale { "classic": string; "muteThread": string; "unmuteThread": string; - "ffVisibility": string; - "ffVisibilityDescription": string; + "followingVisibility": string; + "followersVisibility": string; "continueThread": string; "deleteAccountConfirm": string; "incorrectPassword": string; @@ -947,6 +960,8 @@ export interface Locale { "document": string; "numberOfPageCache": string; "numberOfPageCacheDescription": string; + "numberOfReplies": string; + "numberOfRepliesDescription": string; "logoutConfirm": string; "lastActiveDate": string; "statusbar": string; @@ -1051,6 +1066,8 @@ export interface Locale { "sensitiveWords": string; "sensitiveWordsDescription": string; "sensitiveWordsDescription2": string; + "hiddenTags": string; + "hiddenTagsDescription": string; "notesSearchNotAvailable": string; "license": string; "unfavoriteConfirm": string; @@ -1063,6 +1080,7 @@ export interface Locale { "enableChartsForFederatedInstances": string; "showClipButtonInNoteFooter": string; "reactionsDisplaySize": string; + "limitWidthOfReaction": string; "noteIdOrUrl": string; "video": string; "videos": string; @@ -1184,6 +1202,7 @@ export interface Locale { "avatarDecorations": string; "attach": string; "detach": string; + "detachAll": string; "angle": string; "flip": string; "showAvatarDecorations": string; @@ -1195,6 +1214,14 @@ export interface Locale { "signupPendingError": string; "cwNotationRequired": string; "doReaction": string; + "code": string; + "reloadRequiredToApplySettings": string; + "remainingN": string; + "overwriteContentConfirm": string; + "seasonalScreenEffect": string; + "decorate": string; + "addMfmFunction": string; + "enableQuickAddMfmFunction": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -1671,7 +1698,9 @@ export interface Locale { "assignTarget": string; "descriptionOfAssignTarget": string; "manual": string; + "manualRoles": string; "conditional": string; + "conditionalRoles": string; "condition": string; "isConditionalRole": string; "isPublic": string; @@ -1698,6 +1727,7 @@ export interface Locale { }; "_options": { "gtlAvailable": string; + "btlAvailable": string; "ltlAvailable": string; "canPublicNote": string; "canImportNotes": string; @@ -1722,6 +1752,7 @@ export interface Locale { "canHideAds": string; "canSearchNotes": string; "canUseTranslator": string; + "avatarDecorationLimit": string; }; "_condition": { "isLocal": string; @@ -1754,6 +1785,7 @@ export interface Locale { "disposable": string; "mx": string; "smtp": string; + "banned": string; }; "_ffVisibility": { "public": string; @@ -1973,6 +2005,15 @@ export interface Locale { "notification": string; "antenna": string; "channel": string; + "reaction": string; + }; + "_soundSettings": { + "driveFile": string; + "driveFileWarn": string; + "driveFileTypeWarn": string; + "driveFileTypeWarnDescription": string; + "driveFileDurationWarn": string; + "driveFileDurationWarnDescription": string; }; "_ago": { "future": string; @@ -2068,6 +2109,55 @@ export interface Locale { "write:flash": string; "read:flash-likes": string; "write:flash-likes": string; + "read:admin:abuse-user-reports": string; + "write:admin:delete-account": string; + "write:admin:delete-all-files-of-a-user": string; + "read:admin:index-stats": string; + "read:admin:table-stats": string; + "read:admin:user-ips": string; + "read:admin:meta": string; + "write:admin:reset-password": string; + "write:admin:resolve-abuse-user-report": string; + "write:admin:send-email": string; + "read:admin:server-info": string; + "read:admin:show-moderation-log": string; + "read:admin:show-user": string; + "read:admin:show-users": string; + "write:admin:suspend-user": string; + "write:admin:unset-user-avatar": string; + "write:admin:unset-user-banner": string; + "write:admin:unsuspend-user": string; + "write:admin:meta": string; + "write:admin:user-note": string; + "write:admin:roles": string; + "read:admin:roles": string; + "write:admin:relays": string; + "read:admin:relays": string; + "write:admin:invite-codes": string; + "read:admin:invite-codes": string; + "write:admin:announcements": string; + "read:admin:announcements": string; + "write:admin:avatar-decorations": string; + "read:admin:avatar-decorations": string; + "write:admin:federation": string; + "write:admin:account": string; + "read:admin:account": string; + "write:admin:emoji": string; + "read:admin:emoji": string; + "write:admin:queue": string; + "read:admin:queue": string; + "write:admin:promo": string; + "write:admin:drive": string; + "read:admin:drive": string; + "read:admin:stream": string; + "write:admin:ad": string; + "read:admin:ad": string; + "write:invite-codes": string; + "read:invite-codes": string; + "write:clip-favorite": string; + "read:clip-favorite": string; + "read:federation": string; + "write:report-abuse": string; }; "_auth": { "shareAccessTitle": string; @@ -2128,6 +2218,7 @@ export interface Locale { }; "clicker": string; "search": string; + "birthdayFollowings": string; }; "_cw": { "hide": string; @@ -2198,6 +2289,7 @@ export interface Locale { "changeBanner": string; "changeBackground": string; "verifiedLinkDescription": string; + "avatarDecorationMax": string; }; "_exportOrImport": { "allNotes": string; @@ -2329,6 +2421,7 @@ export interface Locale { "pollEnded": string; "newNote": string; "unreadAntennaNote": string; + "roleAssigned": string; "emptyPushNotificationMessage": string; "achievementEarned": string; "testNotification": string; @@ -2350,6 +2443,7 @@ export interface Locale { "pollEnded": string; "receiveFollowRequest": string; "followRequestAccepted": string; + "roleAssigned": string; "achievementEarned": string; "app": string; }; @@ -2455,6 +2549,8 @@ export interface Locale { "createAvatarDecoration": string; "updateAvatarDecoration": string; "deleteAvatarDecoration": string; + "unsetUserAvatar": string; + "unsetUserBanner": string; }; "_fileViewer": { "title": string; @@ -2534,8 +2630,27 @@ export interface Locale { "text": string; "button": string; }; + "_dataSaver": { + "_media": { + "title": string; + "description": string; + }; + "_avatar": { + "title": string; + "description": string; + }; + "_urlPreview": { + "title": string; + "description": string; + }; + "_code": { + "title": string; + "description": string; + }; + }; } declare const locales: { [lang: string]: Locale; }; +export function build(): Locale; export default locales; diff --git a/locales/index.js b/locales/index.js index 67a406d98d..650e552337 100644 --- a/locales/index.js +++ b/locales/index.js @@ -51,33 +51,37 @@ const primaries = { // 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); -const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); +export function build() { + const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); -// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す -const removeEmpty = (obj) => { - for (const [k, v] of Object.entries(obj)) { - if (v === '') { - delete obj[k]; - } else if (typeof v === 'object') { - removeEmpty(v); + // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す + const removeEmpty = (obj) => { + for (const [k, v] of Object.entries(obj)) { + if (v === '') { + delete obj[k]; + } else if (typeof v === 'object') { + removeEmpty(v); + } } - } - return obj; -}; -removeEmpty(locales); + return obj; + }; + removeEmpty(locales); -export default Object.entries(locales) - .reduce((a, [k ,v]) => (a[k] = (() => { - const [lang] = k.split('-'); - switch (k) { - case 'ja-JP': return v; - case 'ja-KS': - case 'en-US': return merge(locales['ja-JP'], v); - default: return merge( - locales['ja-JP'], - locales['en-US'], - locales[`${lang}-${primaries[lang]}`] ?? {}, - v - ); - } - })(), a), {}); + return Object.entries(locales) + .reduce((a, [k, v]) => (a[k] = (() => { + const [lang] = k.split('-'); + switch (k) { + case 'ja-JP': return v; + case 'ja-KS': + case 'en-US': return merge(locales['ja-JP'], v); + default: return merge( + locales['ja-JP'], + locales['en-US'], + locales[`${lang}-${primaries[lang]}`] ?? {}, + v + ); + } + })(), a), {}); +} + +export default build(); diff --git a/locales/it-IT.yml b/locales/it-IT.yml index fcf1a099e7..33686ddc3d 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -15,7 +15,7 @@ gotIt: "ok!" cancel: "Annulla" noThankYou: "No grazie" enterUsername: "Inserisci un nome utente" -renotedBy: "Rinotato da {user}" +renotedBy: "Rinotata da {user}" noNotes: "Nessuna nota!" noNotifications: "Nessuna notifica" instance: "Istanza" @@ -103,12 +103,12 @@ defaultNoteVisibility: "Privacy predefinita delle note" follow: "Segui" followRequest: "Richiesta di follow" followRequests: "Richieste di follow" -unfollow: "Non seguire" +unfollow: "Interrompi following" followRequestPending: "Richiesta in approvazione" enterEmoji: "Inserisci emoji" renote: "Rinota" unrenote: "Elimina la Rinota" -renoted: "Rinotato!" +renoted: "Rinotata!" cantRenote: "È impossibile rinotare questa nota." cantReRenote: "È impossibile rinotare una Rinota." quote: "Citazione" @@ -122,7 +122,12 @@ sensitive: "Allegato esplicito" add: "Aggiungi" reaction: "Reazioni" reactions: "Reazioni" -reactionSetting: "Reazioni visualizzate sul pannello" +emojiPicker: "Selettore emoji" +pinnedEmojisForReactionSettingDescription: "Scegli quale sia l'emoji in cima, quando reagisci" +pinnedEmojisSettingDescription: "Scegli quale sia l'emoji in cima, quando reagisci" +emojiPickerDisplay: "Visualizza selettore" +overwriteFromPinnedEmojisForReaction: "Sovrascrivi con le impostazioni reazioni" +overwriteFromPinnedEmojis: "Sovrascrivi con le impostazioni globali" reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere." rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note" attachCancel: "Rimuovi allegato" @@ -262,6 +267,7 @@ removed: "Eliminato con successo" removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?" deleteAreYouSure: "Vuoi davvero eliminare \"{x}\"?" resetAreYouSure: "Ripristinare?" +areYouSure: "Confermi?" saved: "Salvato" messaging: "Messaggi" upload: "Carica" @@ -312,6 +318,7 @@ folderName: "Nome della cartella" createFolder: "Nuova cartella" renameFolder: "Rinomina cartella" deleteFolder: "Elimina cartella" +folder: "Cartella" addFile: "Allega" emptyDrive: "Il Drive è vuoto" emptyFolder: "La cartella è vuota" @@ -438,7 +445,6 @@ share: "Condividi" notFound: "Non trovato" notFoundDescription: "Nessuna pagina corrisponde all'URL indicata." uploadFolder: "Destinazione caricamento predefinita" -cacheClear: "Svuota cache" markAsReadAllNotifications: "Segna tutte le notifiche come lette" markAsReadAllUnreadNotes: "Segna tutte le note come lette" markAsReadAllTalkMessages: "Segna tutte le chat come lette" @@ -545,6 +551,8 @@ showInPage: "Visualizza in pagina" popout: "Finestra pop-out" volume: "Volume" masterVolume: "Volume principale" +notUseSound: "Non emettere suoni" +useSoundOnlyWhenActive: "Emetti suoni solo quando Misskey è in attività" details: "Dettagli" chooseEmoji: "Scegli emoji" unableToProcess: "Impossibile compiere l'operazione" @@ -565,6 +573,10 @@ output: "Uscita" script: "Script" disablePagesScript: "Disabilita AiScript nelle pagine" updateRemoteUser: "Aggiorna le informazioni dal profilo remoto" +unsetUserAvatar: "Rimozione foto profilo" +unsetUserAvatarConfirm: "Vuoi davvero rimuovere la foto profilo?" +unsetUserBanner: "Rimuovi intestazione profilo" +unsetUserBannerConfirm: "Vuoi davvero rimuovere l'intestazione dal profilo?" deleteAllFiles: "Elimina tutti i file" deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?" removeAllFollowing: "Annulla tutti i follow" @@ -636,6 +648,7 @@ smtpSecure: "Usare SSL/TLS implicito per le connessioni SMTP" smtpSecureInfo: "Disabilitare quando è attivo STARTTLS." testEmail: "Verifica il funzionamento" wordMute: "Filtri parole" +hardWordMute: "Filtro parole forte" regexpError: "errore regex" regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:" instanceMute: "Silenzia l'istanza" @@ -817,7 +830,7 @@ configure: "Imposta" postToGallery: "Pubblicare nella galleria" postToHashtag: "Pubblica a questo hashtag" gallery: "Galleria" -recentPosts: "Le più recenti" +recentPosts: "Pubblicazioni recenti" popularPosts: "Le più visualizzate" shareWithNote: "Condividere in nota" ads: "Banner" @@ -855,7 +868,7 @@ pubSub: "Publish/Subscribe del profilo" lastCommunication: "La comunicazione più recente" resolved: "Risolto" unresolved: "Non risolto" -breakFollow: "Non farti più seguire" +breakFollow: "Interrompi follow" breakFollowConfirm: "Vuoi davvero che questo profilo smetta di seguirti?" itsOn: "Abilitato" itsOff: "Disabilitato" @@ -871,8 +884,6 @@ makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a di classic: "Classico" muteThread: "Silenzia conversazione" unmuteThread: "Riattiva la conversazione" -ffVisibility: "Visibilità delle connessioni" -ffVisibilityDescription: "Puoi scegliere a chi mostrare le tue relazioni con altri profili nel fediverso." continueThread: "Altre conversazioni" deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" incorrectPassword: "La password è errata." @@ -1024,6 +1035,8 @@ resetPasswordConfirm: "Vuoi davvero ripristinare la password?" sensitiveWords: "Parole esplicite" sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga." sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare." +hiddenTags: "Hashtag nascosti" +hiddenTagsDescription: "Impedire la visualizzazione del tag impostato nei trend. Puoi impostare più valori, uno per riga." notesSearchNotAvailable: "Non è possibile cercare tra le Note." license: "Licenza" unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?" @@ -1036,6 +1049,7 @@ enableChartsForRemoteUser: "Abilita i grafici per i profili remoti" enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate" showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note" reactionsDisplaySize: "Grandezza delle reazioni" +limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale" noteIdOrUrl: "ID della Nota o URL" video: "Video" videos: "Video" @@ -1151,6 +1165,7 @@ tosAndPrivacyPolicy: "Condizioni d'uso e informativa privacy" avatarDecorations: "Decorazioni foto profilo" attach: "Applica" detach: "Rimuovi" +detachAll: "Togli tutto" angle: "Angolo" flip: "Inverti" showAvatarDecorations: "Mostra decorazione della foto profilo" @@ -1162,6 +1177,12 @@ useGroupedNotifications: "Mostra le notifiche raggruppate" signupPendingError: "Si è verificato un problema durante la verifica del tuo indirizzo email. Potrebbe essere scaduto il collegamento temporaneo." cwNotationRequired: "Devi indicare perché il contenuto è indicato come esplicito." doReaction: "Reagisci" +code: "Codice" +reloadRequiredToApplySettings: "Per applicare le impostazioni, occorre ricaricare." +remainingN: "Rimangono: {n}" +overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?" +seasonalScreenEffect: "Schermate in base alla stagione" +decorate: "Decora" _announcement: 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." @@ -1270,8 +1291,8 @@ _serverSettings: shortName: "Abbreviazione" shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server." fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare." - fanoutTimelineDbFallback: "Ripiega sul database" - fanoutTimelineDbFallbackDescription: "Attivando questa funzionalità, nel caso che il contenuto di una Timeline non sia presente nella cache, verrà consultato il database. Disattivandola, il carico sul database sarà ulteriormente ridotto, ma le Timeline saranno limitate" + fanoutTimelineDbFallback: "Elaborazione dati alternativa" + fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline." _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" moveFromSub: "Crea un alias verso un altro profilo remoto" @@ -1542,7 +1563,9 @@ _role: assignTarget: "Modalità di assegnazione del ruolo" descriptionOfAssignTarget: "Manuale: per assegnare manualmente questo ruolo ai profili.\nCondizionale: per assegnare o rimuovere automaticamente questo ruolo ai profili, a precise condizioni." manual: "Manuale" + manualRoles: "Ruoli assegnati manualmente" conditional: "Condizionale" + conditionalRoles: "Ruoli condizionati" condition: "Condizioni" isConditionalRole: "Questo è un ruolo condizionato" isPublic: "Ruolo pubblico" @@ -1591,6 +1614,7 @@ _role: canHideAds: "Nascondere i banner" canSearchNotes: "Ricercare nelle Note" canUseTranslator: "Tradurre le Note" + avatarDecorationLimit: "Numero massimo di decorazioni foto profilo installabili" _condition: isLocal: "Profilo locale" isRemote: "Profilo remoto" @@ -1666,7 +1690,7 @@ _preferencesBackups: list: "Elenco di impostazioni salvate in precedenza" saveNew: "Nuovo salvataggio" loadFile: "Carica da file" - apply: "Applicabile a questo dispositivo" + apply: "Applica a questo dispositivo" save: "Sovrascrivi il backup" inputName: "Inserire il nome del backup." cannotSave: "Impossibile salvare." @@ -1813,6 +1837,14 @@ _sfx: notification: "Notifiche" antenna: "Ricezione dell'antenna" channel: "Notifiche di canale" + reaction: "Quando seleziono una reazione" +_soundSettings: + driveFile: "Suoni del Drive" + driveFileWarn: "Seleziona file dal dispositivo" + driveFileTypeWarn: "Formato file non supportato" + driveFileTypeWarnDescription: "Per favore, scegli un file di tipo audio" + driveFileDurationWarn: "La durata dell'audio è troppo lunga" + driveFileDurationWarnDescription: "Scegliere un audio lungo potrebbe interferire con l'uso di Misskey. Vuoi continuare lo stesso?" _ago: future: "Futuro" justNow: "Adesso" @@ -1825,13 +1857,13 @@ _ago: yearsAgo: "{n} anni fa" invalid: "Niente da visualizzare" _timeIn: - seconds: "fra {n} secondi" - minutes: "fra {n} minuti" - hours: "fra {n} ore" - days: "fra {n} giorni" - weeks: "fra {n} settimane" - months: "fra {n} mesi" - years: "fra {n} anni" + seconds: "Dopo {n} secondi" + minutes: "Dopo {n} minuti" + hours: "Dopo {n} ore" + days: "Dopo {n} giorni" + weeks: "Dopo {n} settimane" + months: "Dopo {n} mesi" + years: "Dopo {n} anni" _time: second: "s" minute: "min" @@ -1876,7 +1908,7 @@ _permissions: "read:favorites": "Visualizza i tuoi preferiti" "write:favorites": "Gestisci i tuoi preferiti" "read:following": "Vedi le informazioni di follow" - "write:following": "Seguire / Non seguire altri profili" + "write:following": "Following di altri profili" "read:messaging": "Visualizzare la chat" "write:messaging": "Gestire la chat" "read:mutes": "Vedi i profili silenziati" @@ -1957,6 +1989,7 @@ _widgets: _userList: chooseList: "Seleziona una lista" clicker: "Cliccaggio" + birthdayFollowings: "Chi nacque oggi" _cw: hide: "Nascondere" show: "Continua la lettura..." @@ -2019,6 +2052,7 @@ _profile: changeAvatar: "Modifica immagine profilo" changeBanner: "Cambia intestazione" verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo." + avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni." _exportOrImport: allNotes: "Tutte le note" favoritedNotes: "Note preferite" @@ -2255,6 +2289,8 @@ _moderationLogTypes: createAvatarDecoration: "Creazione decorazione della foto profilo" updateAvatarDecoration: "Aggiornamento decorazione foto profilo" deleteAvatarDecoration: "Eliminazione decorazione della foto profilo" + unsetUserAvatar: "Rimossa foto profilo" + unsetUserBanner: "Rimossa intestazione profilo" _fileViewer: title: "Dettagli del file" type: "Tipo di file" @@ -2304,3 +2340,16 @@ _externalResourceInstaller: _themeInstallFailed: title: "Impossibile installare la variazione grafica" description: "Si è verificato un impedimento durante l'installazione della variazione grafica. Per favore riprova e consulta la console di Javascript per ottenere dettagli aggiuntivi." +_dataSaver: + _media: + title: "Caricamento dei media" + description: "Impedire il caricamento automatico di immagini e video. Devi toccare le immagini o i video nascosti per caricarli." + _avatar: + title: "Immagine del profilo" + description: "Impedire l'animazione per l'immagine del profilo. Le immagini animate possono avere dimensioni file maggiori rispetto a quelle normali, puoi ridurre ulteriormente l'utilizzo dei dati." + _urlPreview: + title: "Anteprime delle URL" + description: "Impedire il caricamento delle anteprime URL." + _code: + title: "Codice evidenziato" + description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato." diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2f81a25cba..b632fbad63 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -125,7 +125,12 @@ sensitive: "センシティブ" add: "追加" reaction: "リアクション" reactions: "リアクション" -reactionSetting: "ピッカーに表示するリアクション" +emojiPicker: "絵文字ピッカー" +pinnedEmojisForReactionSettingDescription: "リアクション時にピン留め表示する絵文字を設定できます" +pinnedEmojisSettingDescription: "絵文字入力時にピン留め表示する絵文字を設定できます" +emojiPickerDisplay: "ピッカーの表示" +overwriteFromPinnedEmojisForReaction: "リアクション設定から上書きする" +overwriteFromPinnedEmojis: "全般設定から上書きする" reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。" rememberNoteVisibility: "公開範囲を記憶する" attachCancel: "添付取り消し" @@ -271,6 +276,7 @@ removed: "削除しました" removeAreYouSure: "「{x}」を削除しますか?" deleteAreYouSure: "「{x}」を削除しますか?" resetAreYouSure: "リセットしますか?" +areYouSure: "よろしいですか?" saved: "保存しました" messaging: "チャット" upload: "アップロード" @@ -321,6 +327,7 @@ folderName: "フォルダー名" createFolder: "フォルダーを作成" renameFolder: "フォルダー名を変更" deleteFolder: "フォルダーを削除" +folder: "フォルダー" addFile: "ファイルを追加" emptyDrive: "ドライブは空です" emptyFolder: "フォルダーは空です" @@ -448,7 +455,6 @@ share: "共有" notFound: "見つかりません" notFoundDescription: "指定されたURLに該当するページはありませんでした。" uploadFolder: "既定アップロード先" -cacheClear: "キャッシュを削除" markAsReadAllNotifications: "すべての通知を既読にする" markAsReadAllUnreadNotes: "すべての投稿を既読にする" markAsReadAllTalkMessages: "すべてのチャットを既読にする" @@ -558,6 +564,8 @@ showInPage: "ページで表示" popout: "ポップアウト" volume: "音量" masterVolume: "マスター音量" +notUseSound: "サウンドを出力しない" +useSoundOnlyWhenActive: "Misskeyがアクティブな時のみサウンドを出力する" details: "詳細" chooseEmoji: "絵文字を選択" unableToProcess: "操作を完了できません" @@ -578,6 +586,10 @@ output: "出力" script: "スクリプト" disablePagesScript: "Pagesのスクリプトを無効にする" updateRemoteUser: "リモートユーザー情報の更新" +unsetUserAvatar: "アイコンを解除" +unsetUserAvatarConfirm: "アイコンを解除しますか?" +unsetUserBanner: "バナーを解除" +unsetUserBannerConfirm: "バナーを解除しますか?" deleteAllFiles: "すべてのファイルを削除" deleteAllFilesConfirm: "すべてのファイルを削除しますか?" removeAllFollowing: "フォローを全解除" @@ -649,6 +661,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecureInfo: "STARTTLS使用時はオフにします。" testEmail: "配信テスト" wordMute: "ワードミュート" +hardWordMute: "ハードワードミュート" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" instanceMute: "サーバーミュート" @@ -885,8 +898,8 @@ makeReactionsPublicDescription: "あなたがしたリアクション一覧を classic: "クラシック" muteThread: "スレッドをミュート" unmuteThread: "スレッドのミュートを解除" -ffVisibility: "つながりの公開範囲" -ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。" +followingVisibility: "フォローの公開範囲" +followersVisibility: "フォロワーの公開範囲" continueThread: "さらにスレッドを見る" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" incorrectPassword: "パスワードが間違っています。" @@ -944,6 +957,8 @@ approvalStatus: "承認状況" document: "ドキュメント" numberOfPageCache: "ページキャッシュ数" numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。" +numberOfReplies: "スレッド内の返信数" +numberOfRepliesDescription: "この数値を大きくすると、より多くの返信が表示されます。この値を大きくしすぎると、返信が窮屈になり、読めなくなることがあります。" logoutConfirm: "ログアウトしますか?" lastActiveDate: "最終利用日時" statusbar: "ステータスバー" @@ -1048,6 +1063,8 @@ resetPasswordConfirm: "パスワードリセットしますか?" sensitiveWords: "センシティブワード" sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" +hiddenTags: "非表示ハッシュタグ" +hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" unfavoriteConfirm: "お気に入り解除しますか?" @@ -1060,6 +1077,7 @@ enableChartsForRemoteUser: "リモートユーザーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" reactionsDisplaySize: "リアクションの表示サイズ" +limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する" noteIdOrUrl: "ノートIDまたはURL" video: "動画" videos: "動画" @@ -1181,6 +1199,7 @@ tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" avatarDecorations: "アイコンデコレーション" attach: "付ける" detach: "外す" +detachAll: "全て外す" angle: "角度" flip: "反転" showAvatarDecorations: "アイコンのデコレーションを表示" @@ -1192,6 +1211,14 @@ useGroupedNotifications: "通知をグルーピングして表示する" signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" doReaction: "リアクションする" +code: "コード" +reloadRequiredToApplySettings: "設定の反映にはリロードが必要です。" +remainingN: "残り: {n}" +overwriteContentConfirm: "現在の内容に上書きされますがよろしいですか?" +seasonalScreenEffect: "季節に応じた画面の演出" +decorate: "デコる" +addMfmFunction: "装飾を追加" +enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -1581,7 +1608,9 @@ _role: assignTarget: "アサイン" descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれるかを手動で管理します。\nコンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。" manual: "マニュアル" + manualRoles: "マニュアルロール" conditional: "コンディショナル" + conditionalRoles: "コンディショナルロール" condition: "条件" isConditionalRole: "これはコンディショナルロールです。" isPublic: "公開ロール" @@ -1607,6 +1636,7 @@ _role: high: "高" _options: gtlAvailable: "グローバルタイムラインの閲覧" + btlAvailable: "バブルのタイムラインを見ることができる" ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" canImportNotes: "ノートのインポートが可能" @@ -1631,6 +1661,7 @@ _role: canHideAds: "広告の非表示" canSearchNotes: "ノート検索の利用" canUseTranslator: "翻訳機能の利用" + avatarDecorationLimit: "アイコンデコレーションの最大取付個数" _condition: isLocal: "ローカルユーザー" isRemote: "リモートユーザー" @@ -1661,6 +1692,7 @@ _emailUnavailable: disposable: "恒久的に使用可能なアドレスではありません" mx: "正しいメールサーバーではありません" smtp: "メールサーバーが応答しません" + banned: "このメールアドレスでは登録できません" _ffVisibility: public: "公開" @@ -1878,6 +1910,15 @@ _sfx: notification: "通知" antenna: "アンテナ受信" channel: "チャンネル通知" + reaction: "リアクション選択時" + +_soundSettings: + driveFile: "ドライブの音声を使用" + driveFileWarn: "ドライブのファイルを選択してください" + driveFileTypeWarn: "このファイルは対応していません" + driveFileTypeWarnDescription: "音声ファイルを選択してください" + driveFileDurationWarn: "音声が長すぎます" + driveFileDurationWarnDescription: "長い音声を使用するとMisskeyの使用に支障をきたす可能性があります。それでも続行しますか?" _ago: future: "未来" @@ -1889,7 +1930,7 @@ _ago: weeksAgo: "{n}週間前" monthsAgo: "{n}ヶ月前" yearsAgo: "{n}年前" - invalid: "ありません" + invalid: "日時の解析に失敗" _timeIn: seconds: "{n}秒後" @@ -1973,6 +2014,55 @@ _permissions: "write:flash": "Playを操作する" "read:flash-likes": "Playのいいねを見る" "write:flash-likes": "Playのいいねを操作する" + "read:admin:abuse-user-reports": "ユーザーからの通報を見る" + "write:admin:delete-account": "ユーザーアカウントを削除する" + "write:admin:delete-all-files-of-a-user": "ユーザーのすべてのファイルを削除する" + "read:admin:index-stats": "データベースインデックスに関する情報を見る" + "read:admin:table-stats": "データベーステーブルに関する情報を見る" + "read:admin:user-ips": "ユーザーのIPアドレスを見る" + "read:admin:meta": "インスタンスのメタデータを見る" + "write:admin:reset-password": "ユーザーのパスワードをリセットする" + "write:admin:resolve-abuse-user-report": "ユーザーからの通報を解決する" + "write:admin:send-email": "メールを送る" + "read:admin:server-info": "サーバーの情報を見る" + "read:admin:show-moderation-log": "モデレーションログを見る" + "read:admin:show-user": "ユーザーのプライベートな情報を見る" + "read:admin:show-users": "ユーザーのプライベートな情報を見る" + "write:admin:suspend-user": "ユーザーを凍結する" + "write:admin:unset-user-avatar": "ユーザーのアバターを削除する" + "write:admin:unset-user-banner": "ユーザーのバーナーを削除する" + "write:admin:unsuspend-user": "ユーザーの凍結を解除する" + "write:admin:meta": "インスタンスのメタデータを操作する" + "write:admin:user-note": "モデレーションノートを操作する" + "write:admin:roles": "ロールを操作する" + "read:admin:roles": "ロールを見る" + "write:admin:relays": "リレーを操作する" + "read:admin:relays": "リレーを見る" + "write:admin:invite-codes": "招待コードを操作する" + "read:admin:invite-codes": "招待コードを見る" + "write:admin:announcements": "お知らせを操作する" + "read:admin:announcements": "お知らせを見る" + "write:admin:avatar-decorations": "アバターデコレーションを操作する" + "read:admin:avatar-decorations": "アバターデコレーションを見る" + "write:admin:federation": "連合に関する情報を操作する" + "write:admin:account": "ユーザーアカウントを操作する" + "read:admin:account": "ユーザーに関する情報を見る" + "write:admin:emoji": "絵文字を操作する" + "read:admin:emoji": "絵文字を見る" + "write:admin:queue": "ジョブキューを操作する" + "read:admin:queue": "ジョブキューに関する情報を見る" + "write:admin:promo": "プロモーションノートを操作する" + "write:admin:drive": "ユーザーのドライブを操作する" + "read:admin:drive": "ユーザーのドライブの関する情報を見る" + "read:admin:stream": "管理者用のWebsocket APIを使う" + "write:admin:ad": "広告を操作する" + "read:admin:ad": "広告を見る" + "write:invite-codes": "招待コードを作成する" + "read:invite-codes": "招待コードを取得する" + "write:clip-favorite": "クリップのいいねを操作する" + "read:clip-favorite": "クリップのいいねを見る" + "read:federation": "連合に関する情報を取得する" + "write:report-abuse": "違反を報告する" _auth: shareAccessTitle: "アプリへのアクセス許可" @@ -2032,6 +2122,7 @@ _widgets: chooseList: "リストを選択" clicker: "クリッカー" search: "検索" + birthdayFollowings: "今日誕生日のユーザー" _cw: hide: "隠す" @@ -2101,6 +2192,7 @@ _profile: changeBanner: "バナー画像を変更" changeBackground: "背景を変更する" verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" + avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" _exportOrImport: allNotes: "全てのノート" @@ -2231,6 +2323,7 @@ _notification: pollEnded: "アンケートの結果が出ました" newNote: "新しい投稿" unreadAntennaNote: "アンテナ {name}" + roleAssigned: "ロールが付与されました" emptyPushNotificationMessage: "プッシュ通知の更新をしました" achievementEarned: "実績を獲得" testNotification: "通知テスト" @@ -2253,6 +2346,7 @@ _notification: pollEnded: "アンケートが終了" receiveFollowRequest: "フォロー申請を受け取った" followRequestAccepted: "フォローが受理された" + roleAssigned: "ロールが付与された" achievementEarned: "実績の獲得" app: "連携アプリからの通知" @@ -2356,6 +2450,8 @@ _moderationLogTypes: createAvatarDecoration: "アイコンデコレーションを作成" updateAvatarDecoration: "アイコンデコレーションを更新" deleteAvatarDecoration: "アイコンデコレーションを削除" + unsetUserAvatar: "ユーザーのアイコンを解除" + unsetUserBanner: "ユーザーのバナーを解除" _fileViewer: title: "ファイルの詳細" @@ -2420,3 +2516,17 @@ _dataRequest: warn: "データのリクエストは3日ごとにしかできない。" text: "データのダウンロードが完了すると、このアカウントに登録されているEメールアドレスにEメールが送信されます。" button: "リクエスト" + +_dataSaver: + _media: + title: "メディアの読み込み" + description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。" + _avatar: + title: "アイコン画像" + description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。" + _urlPreview: + title: "URLプレビューのサムネイル" + description: "URLプレビューのサムネイル画像が読み込まれなくなります。" + _code: + title: "コードハイライト" + description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 61ab5675d2..1a78c1ec46 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -15,7 +15,7 @@ gotIt: "ほい" cancel: "やめとく" noThankYou: "やめとく" enterUsername: "ユーザー名を入れてや" -renotedBy: "{user}がRenoteしたで" +renotedBy: "{user}がリノートしたで" noNotes: "ノートはあらへん" noNotifications: "通知はあらへん" instance: "サーバー" @@ -38,7 +38,7 @@ addUser: "ユーザーを追加や" favorite: "お気に入り" favorites: "お気に入り" unfavorite: "やっぱ気に入らん" -favorited: "お気に入りに入れたで" +favorited: "お気に入りに入れたで。" alreadyFavorited: "もうお気に入りに入れとるがな。" cantFavorite: "アカン、お気に入りに入れれんかったわ。" pin: "ピン留めしとく" @@ -48,9 +48,9 @@ copyLink: "リンクをコピー" copyLinkRenote: "リノートのリンクをコピーするで?" delete: "ほかす" deleteAndEdit: "ほかして直す" -deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん?" +deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、リノート、返信も全部消えるんやけどそれでもええん?" addToList: "リストに入れたる" -addToAntenna: "アンテナに追加" +addToAntenna: "アンテナに入れる" sendMessage: "メッセージを送る" copyRSS: "RSSをコピー" copyUsername: "ユーザー名をコピー" @@ -59,7 +59,7 @@ copyNoteId: "ノートIDをコピー" copyFileId: "ファイルIDをコピー" copyFolderId: "フォルダーIDをコピー" copyProfileUrl: "プロフィールURLをコピー" -searchUser: "ユーザーを検索" +searchUser: "ユーザーを探す" reply: "返事" loadMore: "まだまだあるで!" showMore: "まだまだあるで!" @@ -68,7 +68,7 @@ youGotNewFollower: "フォローされたで" receiveFollowRequest: "フォローリクエストされたで" followRequestAccepted: "フォローが承認されたで" mention: "メンション" -mentions: "自分宛て" +mentions: "あんた宛て" directNotes: "ダイレクト投稿" importAndExport: "インポートとエクスポート" import: "インポート" @@ -88,7 +88,7 @@ followers: "フォロワー" followsYou: "フォローされとるで" createList: "リスト作る" manageLists: "リストの管理" -error: "エラー" +error: "おかしなったで" somethingHappened: "なんかあかんわ" retry: "もっぺんやる?" pageLoadError: "ページが読み込めんかったわ。" @@ -105,13 +105,13 @@ followRequests: "フォロー申請" unfollow: "フォローやめる" followRequestPending: "フォロー許してくれるん待っとる" enterEmoji: "絵文字を入れてや" -renote: "Renote" -unrenote: "Renoteやめる" -renoted: "Renoteしたで。" -cantRenote: "この投稿はRenoteできへんらしい。" -cantReRenote: "Renote自体はRenoteできへんで。" +renote: "リノート" +unrenote: "リノートやめる" +renoted: "リノートしたで。" +cantRenote: "この投稿はリノートできへんっぽい。" +cantReRenote: "リノート自体はリノートできへんで。" quote: "引用" -inChannelRenote: "チャンネル内Renote" +inChannelRenote: "チャンネルの中でリノート" inChannelQuote: "チャンネル内引用" pinnedNote: "ピン留めされとるノート" pinned: "ピン留めしとく" @@ -121,7 +121,12 @@ sensitive: "気いつけて見いや" add: "増やす" reaction: "ツッコミ" reactions: "ツッコミ" -reactionSetting: "ピッカーに出しとくツッコミ" +emojiPicker: "絵文字ピッカー" +pinnedEmojisForReactionSettingDescription: "リアクションしたときにピンで留めてる表示をする絵文字を設定するで" +pinnedEmojisSettingDescription: "絵文字打ったときにピン留め表示する絵文字設定できるで" +emojiPickerDisplay: "ピッカーの表示" +overwriteFromPinnedEmojisForReaction: "リアクション設定から上書きする" +overwriteFromPinnedEmojis: "全般設定から上書きする" reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。" rememberNoteVisibility: "公開範囲覚えといて" attachCancel: "のっけるのやめる" @@ -130,8 +135,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ" enterFileName: "ファイル名を入れてや" mute: "ミュート" unmute: "ミュートやめたる" -renoteMute: "Renoteは見いひん" -renoteUnmute: "Renoteもやっぱ見るわ" +renoteMute: "リノートは見いひん" +renoteUnmute: "リノートもやっぱ見るわ" block: "ブロック" unblock: "ブロックやめたる" suspend: "凍結" @@ -141,13 +146,13 @@ unblockConfirm: "ブロックやめたるってほんまか?" suspendConfirm: "凍結してしもうてええか?" unsuspendConfirm: "解凍するけどええか?" selectList: "リストを選ぶ" -editList: "リスト直すで" +editList: "リストいじる" selectChannel: "チャンネルを選ぶ" selectAntenna: "アンテナを選ぶ" -editAntenna: "アンテナを編集" +editAntenna: "アンテナいじる" selectWidget: "ウィジェットを選ぶ" editWidgets: "ウィジェットをいじる" -editWidgetsExit: "編集終ったで" +editWidgetsExit: "いじるのをやめる" customEmojis: "カスタム絵文字" emoji: "絵文字" emojis: "絵文字" @@ -156,14 +161,14 @@ emojiUrl: "絵文字画像URL" addEmoji: "絵文字を追加" settingGuide: "ええ感じの設定" cacheRemoteFiles: "リモートのファイルをキャッシュする" -cacheRemoteFilesDescription: "この設定を切っとったら、リモートファイルをキャッシュせんと直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルを作らんなるから通信量が増えるで。" +cacheRemoteFilesDescription: "この設定を入れとったら、リモートのファイルを端から端までこのサーバーのキャッシュん中突っ込むようになるで。画像映し出すんがめっちゃ速うなるけど、サーバーの容量をやたらと食うようになるで。リモートの人がどんだけ長くキャッシュを持っとくかはドライブ容量の制限で決めとくで。制限を超えたら古いのから順々に消してって、かわりにリンクになるで。この設定を切ったら、リモートのファイルは最初っからリンクとして扱うことにするけど、画像のサムネ作るのとかみんなのプライバシー守るために、default.ymlのproxyRemoteFilesをtrueにしといたほうがええよ。" youCanCleanRemoteFilesCache: "ファイル管理にある🗑️ボタンでキャッシュ全部ほかすで。" -cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする" -cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになるで。" +cacheRemoteSensitiveFiles: "リモートのきわどいファイルをキャッシュに突っ込む" +cacheRemoteSensitiveFilesDescription: "この設定を切ると、リモートのきわどいファイルはキャッシュせず直でリンクするようになるで。" flagAsBot: "Botにするで" flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Sharkeyのシステム上での扱いがBotに合ったもんになるからな。" -flagAsCat: "Catやで" -flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?" +flagAsCat: "猫や。かわええな。" +flagAsCatDescription: "ネコになりたいんならこれつけとき。" flagSpeakAsCat: "猫語で話すで" flagSpeakAsCatDescription: "有効にすると、あなたの投稿の 「な」を「にゃ」にするでー。" flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで" @@ -216,12 +221,12 @@ clearQueueConfirmText: "未配達の投稿は配送されんなるで。ふつ clearCachedFiles: "キャッシュをほかす" clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?" blockedInstances: "ブロックしたサーバー" -blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。" +blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。" silencedInstances: "サーバーサイレンスされてんねん" silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。" muteAndBlock: "ミュートとブロック" -mutedUsers: "ミュートしたユーザー" -blockedUsers: "ブロックしたユーザー" +mutedUsers: "ミュートしとるユーザー" +blockedUsers: "ブロックしとるユーザー" noUsers: "ユーザーはおらん" editProfile: "プロフィールをいじる" noteDeleteConfirm: "このノートをほかしてええか?" @@ -248,7 +253,7 @@ changePassword: "パスワードをいじる" security: "セキュリティ" retypedNotMatch: "入れたやつ合うてへんわ。" currentPassword: "今のパスワード" -newPassword: "次のパスワード" +newPassword: "今度のパスワード" newPasswordRetype: "今度のパスワード(もっぺん入れて)" attachFile: "ファイルのっける" more: "他のん" @@ -263,6 +268,7 @@ removed: "ほかしたで!" removeAreYouSure: "「{x}」はほかしてええか?" deleteAreYouSure: "「{x}」はほかしてええか?" resetAreYouSure: "リセットしてええん?" +areYouSure: "いいん?" saved: "保存したで!" messaging: "チャット" upload: "アップロード" @@ -313,6 +319,7 @@ folderName: "フォルダー名" createFolder: "フォルダー作る" renameFolder: "フォルダー名を変える" deleteFolder: "フォルダーをほかす" +folder: "フォルダー" addFile: "ファイルを追加" emptyDrive: "ドライブは空っぽや" emptyFolder: "このフォルダーは空や" @@ -326,7 +333,7 @@ copyUrl: "URLをコピー" rename: "名前を変えるで" avatar: "アイコン" banner: "バナー" -displayOfSensitiveMedia: "センシティブなメディアの表示" +displayOfSensitiveMedia: "きわどいやつの表示" whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき" disconnectedFromServer: "サーバーが機嫌悪いねん" reload: "リロード" @@ -414,7 +421,7 @@ userList: "リスト" about: "情報" aboutMisskey: "Sharkeyってなんや?" administrator: "管理者" -token: "トークン" +token: "確認コード" 2fa: "二要素認証" setupOf2fa: "二要素認証のセットアップ" totp: "認証アプリ" @@ -427,7 +434,7 @@ moderationLogs: "モデログ" nUsersMentioned: "{n}人が投稿" securityKeyAndPasskey: "セキュリティキー・パスキー" securityKey: "セキュリティキー" -lastUsed: "最後につこうた日" +lastUsed: "最後に使うた日" lastUsedAt: "最後に使うたんは: {t}" unregister: "登録やめる" passwordLessLogin: "パスワード無くてもログインできるようにする" @@ -439,7 +446,6 @@ share: "わけわけ" notFound: "見つからへんね" notFoundDescription: "言われたURLにはまるページはなかったで。" uploadFolder: "とりあえずアップロードしたやつ置いとく所" -cacheClear: "キャッシュをほかす" markAsReadAllNotifications: "通知はもう全て読んだわっ" markAsReadAllUnreadNotes: "投稿は全て読んだわっ" markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ" @@ -473,7 +479,7 @@ weakPassword: "へぼいパスワード" normalPassword: "ぼちぼちのパスワード" strongPassword: "ええ感じのパスワード" passwordMatched: "よし!一致や!" -passwordNotMatched: "一致しとらんで?" +passwordNotMatched: "ちゃうで?" signinWith: "{x}でログイン" signinFailed: "ログインできんかったで。もっかいユーザー名とパスワードを確認してみてや。" or: "それか" @@ -527,7 +533,7 @@ objectStorageEndpointDesc: "S3のときは空、それ以外は各サービス objectStorageRegion: "Region" objectStorageRegionDesc: "'xx-east-1'みたいなregionを指定したってやー。使ってるサービスにregionの概念がないときは、空か'us-east-1'にするんやで。" objectStorageUseSSL: "SSLを使う" -objectStorageUseSSLDesc: "API接続にhttpsを使わん場合はオフにするんやで" +objectStorageUseSSLDesc: "API接続にhttpsを使わんのやったら消しといて" objectStorageUseProxy: "Proxyを使う" objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん?" objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや" @@ -538,14 +544,16 @@ showFixedPostForm: "タイムラインの上の方で投稿できるようにや showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)" withRepliesByDefaultForNewlyFollowed: "フォローする時、デフォルトで返信をタイムラインに含むようにしよか" newNoteRecived: "新しいノートがあるで" -sounds: "サウンド" -sound: "サウンド" +sounds: "音" +sound: "音" listen: "聴く" none: "なし" showInPage: "ページで表示" popout: "ポップアウト" volume: "やかましさ" masterVolume: "全体のやかましさ" +notUseSound: "音出さへん" +useSoundOnlyWhenActive: "Sharkeyがアクティブなときだけ音出す" details: "もっと" chooseEmoji: "絵文字を選ぶ" unableToProcess: "なんか奥の方で詰まってもうた" @@ -566,6 +574,10 @@ output: "出力" script: "スクリプト" disablePagesScript: "Pagesのスクリプトを無効にしてや" updateRemoteUser: "リモートユーザー情報の更新してくれん?" +unsetUserAvatar: "アイコン戻す" +unsetUserAvatarConfirm: "アイコンを元に戻すで?" +unsetUserBanner: "バナー戻す" +unsetUserBannerConfirm: "バナー元に戻すで?" deleteAllFiles: "ファイルを全部ほかす" deleteAllFilesConfirm: "ホンマにファイル全部ほかすんか?消したもんはもう戻ってこんのやで?" removeAllFollowing: "フォローを全解除" @@ -577,7 +589,7 @@ yourAccountSuspendedDescription: "あんたのアカウントは、サーバー tokenRevoked: "トークンが無効やで" tokenRevokedDescription: "ログイントークンが失効しとるで。もっかいログインしてもろてもええか?" accountDeleted: "アカウントは削除されとるで" -accountDeletedDescription: "このアカウントは削除されとるで。" +accountDeletedDescription: "このアカウントはもう消えとる。" menu: "メニュー" divider: "分割線" addItem: "項目を追加" @@ -593,9 +605,9 @@ enableInfiniteScroll: "自動でもっと見る" visibility: "公開範囲" poll: "アンケート" useCw: "内容を隠す" -enablePlayer: "プレイヤーを開く" -disablePlayer: "プレイヤーを閉じる" -expandTweet: "ポストを展開する" +enablePlayer: "プレイヤー開く" +disablePlayer: "プレイヤー閉じる" +expandTweet: "ポスト展開しとく" themeEditor: "テーマエディター" description: "説明" describeFile: "キャプションを付ける" @@ -608,7 +620,7 @@ preferencesBackups: "設定のバックアップ" deck: "デッキ" undeck: "デッキ解除" useBlurEffectForModal: "モーダルにぼかし効果を使用" -useFullReactionPicker: "フル機能の突っ込みピッカーを使用" +useFullReactionPicker: "フルフルのツッコミピッカーを使う" width: "幅" height: "高さ" large: "大" @@ -637,6 +649,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecureInfo: "STARTTLS使っとる時はオフにしてや。" testEmail: "配信テスト" wordMute: "ワードミュート" +hardWordMute: "ハードワードミュート" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:" instanceMute: "サーバーミュート" @@ -652,7 +665,7 @@ database: "データベース" channel: "チャンネル" create: "作成" notificationSetting: "通知設定" -notificationSettingDesc: "表示する通知の種類えらんでや。" +notificationSettingDesc: "出す通知の種類えらんでや。" useGlobalSetting: "グローバル設定を使ってや" useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使われるで。オフにすると、別々に設定できるようになるで。" other: "その他" @@ -689,18 +702,18 @@ clip: "クリップ" createNew: "新しく作るで" optional: "任意" createNewClip: "新しいクリップを作るで" -unclip: "クリップ解除するで" -confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外しよか?" +unclip: "クリップやめとく" +confirmToUnclipAlreadyClippedNote: "このノートはもう「{name}」に含まれとるで。ノート、このクリップから外そか?" public: "パブリック" private: "非公開" -i18nInfo: "Sharkeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。" +i18nInfo: "Sharkeyは有志がいろんな言語に訳しとるで。{link}で翻訳に協力したってやー。" manageAccessTokens: "アクセストークンの管理" accountInfo: "アカウント情報" notesCount: "ノートの数やで" repliesCount: "返信した数やで" -renotesCount: "Renoteした数やで" +renotesCount: "リノートした数やで" repliedCount: "返信された数やで" -renotedCount: "Renoteされた数やで" +renotedCount: "リノートされた数やで" followingCount: "フォロー数やで" followersCount: "フォロワー数やで" sentReactionsCount: "ツッコんだ数" @@ -717,7 +730,7 @@ lockedAccountInfo: "フォローを承認制にしとっても、ノートの公 alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで" loadRawImages: "添付画像のサムネイルをオリジナル画質にするで" disableShowingAnimatedImages: "アニメーション画像を再生せんとくで" -highlightSensitiveMedia: "メディアがセンシティブなことをめっっちゃわかりやすく表紙" +highlightSensitiveMedia: "きわどいことをめっっちゃわかりやすくする" verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。" notSet: "未設定" emailVerified: "メールアドレスは確認されたで" @@ -729,7 +742,7 @@ useSystemFont: "システムのデフォルトのフォントを使うで" clips: "クリップ" experimentalFeatures: "おためし機能やで" experimental: "実験的" -thisIsExperimentalFeature: "これは実験的な機能やで。仕様が変更になったりちゃんと動かなかったりするかもやで。" +thisIsExperimentalFeature: "これは実験的な機能やから、仕様が変わったりちゃんと動かんかったりするかもしれん。" developer: "開発者やで" makeExplorable: "アカウントを見つけやすくするで" makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。" @@ -747,7 +760,7 @@ onlineUsersCount: "{n}人が起きとるで" nUsers: "{n}ユーザー" nNotes: "{n}ノート" sendErrorReports: "エラーリポートを送る" -sendErrorReportsDescription: "オンにしたら、変なことが起きたときにエラーの詳細がSharkeyに送られて、ソフトウェアの品質向上に使えるようになるで。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴なんかが含まれるで。" +sendErrorReportsDescription: "オンにしたら、なんか変なことが起きたとき、詳しいのが全部Sharkeyに送られて、ソフトウェアをもっと良うするで。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴なんかが含まれるな。" myTheme: "マイテーマ" backgroundColor: "背景" accentColor: "アクセント" @@ -763,8 +776,8 @@ deleteConfirm: "ホンマにほかすで?" invalidValue: "有効な値じゃないみたいやで。" registry: "レジストリ" closeAccount: "アカウントを閉鎖する" -currentVersion: "現在のバージョン" -latestVersion: "最新のバージョン" +currentVersion: "今のやつ" +latestVersion: "いっちゃん新しいやつ" youAreRunningUpToDateClient: "今使ってるクライアントが最新やで!" newVersionOfClientAvailable: "新しいバージョンのクライアントが使えるで。" usageAmount: "使用量" @@ -786,9 +799,9 @@ goBack: "戻る" unlikeConfirm: "いいね解除するんか?" fullView: "フルビュー" quitFullView: "フルビュー解除" -addDescription: "説明を追加するで" -userPagePinTip: "個々のノートのメニューから「ピン留め」を選んどくと、ここにノートを表示しておけるで。" -notSpecifiedMentionWarning: "宛先に含まれてへんメンションがあるで" +addDescription: "説明を入れるで" +userPagePinTip: "ノートのメニューから「ピン留め」を選んどいたら、ここにノートを置いとけるで。" +notSpecifiedMentionWarning: "宛先にないメンションがあるで" info: "情報" userInfo: "ユーザー情報やで" unknown: "不明" @@ -800,7 +813,7 @@ active: "アクティブ" offline: "オフライン" notRecommended: "あんま推奨しやんで" botProtection: "Botプロテクション" -instanceBlocking: "サーバーブロック" +instanceBlocking: "サーバーブロック・サイレンス" selectAccount: "アカウントを選んでなー" switchAccount: "アカウントを変えるで" enabled: "有効" @@ -860,7 +873,7 @@ itsOn: "オンになっとるよ" itsOff: "オフになってるで" on: "オン" off: "オフ" -emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで" +emailRequiredForSignup: "アカウント作るのにメールアドレスを必須にするで" unread: "未読" filter: "フィルタ" controlPanel: "コントロールパネル" @@ -870,11 +883,9 @@ makeReactionsPublicDescription: "あんたがしたツッコミ一覧を誰で classic: "クラシック" muteThread: "スレッドをミュート" unmuteThread: "スレッドのミュートを解除" -ffVisibility: "つながりの公開範囲" -ffVisibilityDescription: "あんたのフォロー/フォロワー情報の公開範囲を設定できるで。" continueThread: "さらにスレッドを見るで" deleteAccountConfirm: "アカウントを消すで?ええんか?" -incorrectPassword: "パスワードがちゃうで。" +incorrectPassword: "パスワードがちゃうわ。" voteConfirm: "「{choice}」に投票するんか?" hide: "隠す" useDrawerReactionPickerForMobile: "ケータイとかのときドロワーで表示するで" @@ -902,8 +913,8 @@ oneMonth: "1ヶ月" reflectMayTakeTime: "反映されるまで時間がかかることがあるで" failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…" rateLimitExceeded: "レート制限が超えたみたいやで" -cropImage: "画像のクロップ" -cropImageAsk: "画像をクロップしてもええか?" +cropImage: "画像切り取り" +cropImageAsk: "画像を切り取ってもええか?" cropYes: "切り抜いたる" cropNo: "切り抜かへん" file: "ファイル" @@ -914,18 +925,18 @@ thereIsUnresolvedAbuseReportWarning: "未対応の通報があるみたいやで recommended: "推奨" check: "チェック" driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更するで" -driveCapOverrideCaption: "0以下を指定すると解除されるで。" -requireAdminForView: "これを見るには管理者アカウントでログインしとらなあかんで。" +driveCapOverrideCaption: "0以下にしたら解除されるで。" +requireAdminForView: "これ見たいんなら管理者じゃないとアカンわ。" isSystemAccount: "システムが自動で作成・管理しとるアカウントやで。" -typeToConfirm: "この操作をやるんなら {x} と入力してなー" +typeToConfirm: "これやるんなら {x} って入力してなー" deleteAccount: "アカウント削除するで" document: "ドキュメント" numberOfPageCache: "ページ、どんだけキャッシュすんの?" -numberOfPageCacheDescription: "増やすと使いやすくなる、負荷とメモリ使用量が増えてくで。一長一短やな。" +numberOfPageCacheDescription: "増やすと使いやすくなるけど、負荷とメモリ使用量が増えてくで。一長一短やな。" logoutConfirm: "ログアウトしまっか?" lastActiveDate: "最後に使った日時" statusbar: "ステータスバー" -pleaseSelect: "選択したってやー" +pleaseSelect: "選んだってやー" reverse: "反転" colored: "色付き" refreshInterval: "更新間隔" @@ -934,28 +945,28 @@ type: "タイプ" speed: "速度" slow: "遅い" fast: "速い" -sensitiveMediaDetection: "センシティブなメディアの検出" -localOnly: "ローカルのみ" -remoteOnly: "リモートのみ" +sensitiveMediaDetection: "きわどいやつの検出" +localOnly: "ローカルだけ" +remoteOnly: "リモートだけ" failedToUpload: "アップロードに失敗してもうたわ…" -cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたからアップロードできへんわ。" -cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いからアップロードできへんわ。" +cannotUploadBecauseInappropriate: "きわどい内容を含むかもしれへんって言われたからアップロードできへんわ。" +cannotUploadBecauseNoFreeSpace: "ドライブがもうパンパンやからアップロードできへんわ。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルが思うたよりも大きいさかいアップロードできへんでこれ。" beta: "ベータ" -enableAutoSensitive: "自動NSFW判定" +enableAutoSensitive: "自動できわどいか判断する" enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。" -activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。" +activeEmailValidationDescription: "ユーザーのメアドのバリデーションを、捨てアドかどうかとか、ちゃんと通信できるかとかを見るで。切ったら単に文字列として合っとるかどうかだけ見るわ。" navbar: "ナビゲーションバー" shuffle: "シャッフルするで" account: "アカウント" -move: "移動するで" +move: "移すで" pushNotification: "プッシュ通知" subscribePushNotification: "プッシュ通知をオンにするで" unsubscribePushNotification: "プッシュ通知を止めるで" pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで" pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に対応してないみたいやで。" sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで" -sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。" +sendPushNotificationReadMessageCaption: "あんたの端末の電池使う量が増えるかもしれん。" windowMaximize: "最大化" windowMinimize: "最小化" windowRestore: "元に戻す" @@ -965,30 +976,30 @@ tools: "ツール" cannotLoad: "読み込めへんで" numberOfProfileView: "プロフィール表示回数" like: "ええやん!" -unlike: "いいねを解除" +unlike: "いいねやめる" numberOfLikes: "いいね数" show: "表示" neverShow: "今後表示しない" remindMeLater: "また後で" didYouLikeMisskey: "Sharkey気に入ってくれた?" -pleaseDonate: "Sharkeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。" +pleaseDonate: "Sharkeyは{host}が使うとる無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。" roles: "ロール" role: "ロール" noRole: "ロールはありまへん" normalUser: "一般ユーザー" undefined: "未定義" assign: "アサイン" -unassign: "アサインを解除" +unassign: "アサインやめる" color: "色" manageCustomEmojis: "カスタム絵文字の管理" manageAvatarDecorations: "アバターを飾るモンの管理" youCannotCreateAnymore: "これ以上作れなさそうやわ" -cannotPerformTemporary: "一時的に利用できへんで" -cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。" +cannotPerformTemporary: "ちょっといまは使えへんで" +cannotPerformTemporaryDescription: "操作し過ぎてちょっと今は使えへんくしとるで。ちょっと待ってからもっかいやってや。" invalidParamError: "パラメータがエラー言うとりますわ" -invalidParamErrorDescription: "リクエストパラメータに問題があんねん。普通はバグやねんけど、もしかすると入力した文字数が多すぎるとかの可能性もあるから確認してや〜" +invalidParamErrorDescription: "リクエストパラメータが変やわ。だいたいはバグやねんけど、もしかしたら入れた文字が多すぎるとかかもしれんから確認してや〜" permissionDeniedError: "操作が拒否されてもうた。" -permissionDeniedErrorDescription: "自分のアカウントにはこの操作を行う権限があらへんねん" +permissionDeniedErrorDescription: "このアカウントはこれやったらアカンって。" preset: "プリセット" selectFromPresets: "プリセットから選ぶ" achievements: "実績" @@ -998,15 +1009,15 @@ thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。" thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingCancel: "やめとく" thisPostMayBeAnnoyingIgnore: "このまま投稿" -collapseRenotes: "見たことあるRenoteは飛ばして表示するで" +collapseRenotes: "見たことあるリノートは飛ばして表示するで" internalServerError: "サーバー内部エラー" -internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ" -copyErrorInfo: "エラー情報をコピー" +internalServerErrorDescription: "サーバーでなんか変なこと起こっとるわ。" +copyErrorInfo: "エラー情報をコピるで" joinThisServer: "このサーバーに登録するわ" exploreOtherServers: "他のサーバー見てみる" letsLookAtTimeline: "タイムライン見てみーや" disableFederationConfirm: "連合なしにしとくか?" -disableFederationConfirmWarn: "連合なしにしても投稿は非公開にはならへんで。大体の場合は連合なしにする必要はないで。" +disableFederationConfirmWarn: "連合なしにしても投稿が非公開になるわけちゃうで。大体の場合は連合なしにする必要はないで。" disableFederationOk: "連合なしにしとく" invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。" emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ" @@ -1015,14 +1026,16 @@ cannotBeChangedLater: "後からは変えられへんで。" reactionAcceptance: "ツッコミの受け入れ" likeOnly: "いいねだけ" likeOnlyForRemote: "リモートからはいいねだけな" -nonSensitiveOnly: "センシティブじゃないやつだけ" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "センシティブじゃないやつだけ (リモートはいいねだけ)" +nonSensitiveOnly: "いつ見ても大丈夫なやつだけ" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "いつ見ても大丈夫なやつだけ (リモートはいいねだけ)" rolesAssignedToMe: "自分に割り当てられたロール" resetPasswordConfirm: "パスワード作り直すんでええな?" sensitiveWords: "けったいな単語" sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。" sensitiveWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。" -notesSearchNotAvailable: "ノート検索は使われへんで。" +hiddenTags: "見えてへんハッシュタグ" +hiddenTagsDescription: "設定したタグを最近流行りのとこに見えんようにすんで。複数設定するときは改行で区切ってな。" +notesSearchNotAvailable: "なんかノート探せへん。" license: "ライセンス" unfavoriteConfirm: "ほんまに気に入らんの?" myClips: "自分のクリップ" @@ -1033,20 +1046,21 @@ retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへん enableChartsForRemoteUser: "リモートユーザーのチャートを作る" enableChartsForFederatedInstances: "リモートサーバーのチャートを作る" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" -reactionsDisplaySize: "リアクションの表示のでかさ" +reactionsDisplaySize: "ツッコミの表示のでかさ" +limitWidthOfReaction: "ツッコミの最大横幅を制限して、ちっさく表示するで" noteIdOrUrl: "ノートIDかURL" video: "動画" videos: "動画" dataSaver: "データケチケチ" accountMigration: "アカウントのお引っ越し" accountMoved: "このユーザーはさらのアカウントに引っ越したで:" -accountMovedShort: "このアカウントは移行されとるで" +accountMovedShort: "このアカウントは引っ越し済みや" operationForbidden: "この操作はできまへん" -forceShowAds: "常に広告を表示しとく" +forceShowAds: "いっつも広告を映す" addMemo: "メモを足す" editMemo: "メモをいらう" reactionsList: "ツッコミ一覧" -renotesList: "Renote一覧" +renotesList: "リノート一覧" notificationDisplay: "通知見せる" leftTop: "左上" rightTop: "右上" @@ -1058,7 +1072,7 @@ horizontal: "横" position: "位置" serverRules: "サーバールール" pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、下に書いてること確認してな。" -pleaseAgreeAllToContinue: "続けるんやったら、全ての「せやな」にチェック入れてる必要があるで。" +pleaseAgreeAllToContinue: "続けるんやったら、全部にチェック入れとかなアカンで。" continue: "続けるで" preservedUsernames: "予約ユーザー名" preservedUsernamesDescription: "予約しとくユーザー名を行ごとに挙げるで。ここで指定されたユーザー名はアカウント作るときに使えへんくなるけど、管理者は例外や。あと、もうあるアカウントも例外やな。" @@ -1084,29 +1098,29 @@ changeReactionConfirm: "ツッコミを別のに変えるか?" later: "あとで" goToMisskey: "Sharkeyへ" additionalEmojiDictionary: "絵文字の追加辞書" -installed: "インストール済み" +installed: "インストールしとる" branding: "ブランディング" enableServerMachineStats: "サーバーのマシン情報見せびらかすで" enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" turnOffToImprovePerformance: "オフにしたらえらい軽うなるで。" -createInviteCode: "招待コードを作成" -createWithOptions: "オプションを指定して作成" -createCount: "作成数" +createInviteCode: "招待コード作る" +createWithOptions: "オプション決めて作る" +createCount: "作った数" inviteCodeCreated: "招待コード作ったで" inviteLimitExceeded: "招待コード作りすぎやで。" -createLimitRemaining: "作成できる招待コード: 残り {limit} 個やで" -inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できるで。" +createLimitRemaining: "作れる招待コードは残り {limit} 個や" +inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作れるで。" expirationDate: "有効期限" -noExpirationDate: "有効期限を設けへん" -inviteCodeUsedAt: "招待コードが使用された日時" -registeredUserUsingInviteCode: "招待コードを使用したユーザー" +noExpirationDate: "期限なし" +inviteCodeUsedAt: "招待コードが使われた時" +registeredUserUsingInviteCode: "招待コードを使うた人" waitingForMailAuth: "メール認証待ち" -inviteCodeCreator: "招待コードを作成したユーザー" -usedAt: "使用日時" +inviteCodeCreator: "招待コードを作った人" +usedAt: "使った時" unused: "つこてへん" used: "もうつこてる" expired: "期限切れ" -doYouAgree: "同意するんか?" +doYouAgree: "ええんか?" beSureToReadThisAsItIsImportant: "重要やから絶対読んでや。" iHaveReadXCarefullyAndAgree: "「{x}」の内容をよう読んで、同意するで。" dialog: "ダイアログ" @@ -1117,119 +1131,181 @@ pastAnnouncements: "過去のお知らせやで" youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんやろ。" useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。" replies: "返事" -renotes: "Renote" +renotes: "リノート" loadReplies: "返信を見るで" loadConversation: "会話を見るで" pinnedList: "ピン留めしはったリスト" keepScreenOn: "デバイスの画面を常にオンにすんで" -verifiedLink: "このリンク先の所有者であることが確認されたで。" +verifiedLink: "このリンク先の所有者ってわかったわ。" notifyNotes: "投稿を通知" -unnotifyNotes: "投稿の通知を解除すんで" +unnotifyNotes: "投稿の通知やめる" authentication: "認証" -authenticationRequiredToContinue: "続けるには認証をやってや。" +authenticationRequiredToContinue: "続けるんなら認証してや。" dateAndTime: "日時" -showRenotes: "リノートを表示" -edited: "編集し終わってる" -notificationRecieveConfig: "通知を受け取るかの設定" +showRenotes: "リノート出す" +edited: "いじったやつ" +notificationRecieveConfig: "通知もらうかの設定" mutualFollow: "お互いフォローしてんで" -fileAttachedOnly: "ファイル付きのみ" -showRepliesToOthersInTimeline: "タイムラインに他の人への返信とかも含めんで" -hideRepliesToOthersInTimeline: "タイムラインに他の人への返信とかは見ーへんで" -showRepliesToOthersInTimelineAll: "" -hideRepliesToOthersInTimelineAll: "" -confirmShowRepliesAll: "" -confirmHideRepliesAll: "" +fileAttachedOnly: "ファイルのっけてあるやつだけ" +showRepliesToOthersInTimeline: "タイムラインに他の人への返信とかも入れるで" +hideRepliesToOthersInTimeline: "タイムラインに他の人への返信とかは入れへん" +showRepliesToOthersInTimelineAll: "タイムラインに今フォローしとる人全員の返信入れるで" +hideRepliesToOthersInTimelineAll: "タイムラインに今フォローしとる人の返信入れへん" +confirmShowRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れるか?" +confirmHideRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れへんのか?" externalServices: "他のサイトのサービス" impressum: "運営者の情報" impressumUrl: "運営者の情報URL" -impressumDescription: "ドイツなどのほんま1部の国と地域ではな、表示が義務付けられててん。(Impressum)" +impressumDescription: "ドイツとかの一部んところではな、表示が義務付けられてんねん(Impressum)。" privacyPolicy: "プライバシーポリシー" privacyPolicyUrl: "プライバシーポリシーURL" tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" avatarDecorations: "アイコンデコレーション" -attach: "" -detach: "" -angle: "" +attach: "のっける" +detach: "取る" +detachAll: "全部とる" +angle: "角度" flip: "反転" -showAvatarDecorations: "" -releaseToRefresh: "離してリロード" -refreshing: "リロード中" +showAvatarDecorations: "アイコンのデコレーション映す" +releaseToRefresh: "離したらリロード" +refreshing: "リロードしとる" pullDownToRefresh: "引っ張ってリロードするで" disableStreamingTimeline: "タイムラインのリアルタイム更新をやめるで" -useGroupedNotifications: "通知をグルーピングしてだすで" -signupPendingError: "メールアドレスの確認中に問題が起こってえらいこっちゃ。リンクの有効期限が切れてるかもやで" -cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要やで。" -doReaction: "ツッコミすんで" +useGroupedNotifications: "通知をグループ分けして出すで" +signupPendingError: "メアド確認してたらなんか変なことなったわ。リンクの期限切れてるかもしれん。" +cwNotationRequired: "「内容を隠す」んやったら注釈書かなアカンで。" +doReaction: "ツッコむで" +code: "コード" +reloadRequiredToApplySettings: "設定を見るんにはリロードが必要やで。" +remainingN: "残り:{n}" +overwriteContentConfirm: "今の内容に上書きされるけどいい?" +seasonalScreenEffect: "季節にあった画面の動き" +decorate: "デコる" _announcement: forExistingUsers: "もうおるユーザーのみ" - forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" - needConfirmationToRead: "既読にするのに確認が必要やで" - needConfirmationToReadDescription: "有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。また、一括既読操作の対象にもなりません。" - end: "お知らせを終了" - tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討した方がええよ。" + forExistingUsersDescription: "オンにしたらこのお知らせができた時点でおる人らにだけお知らせが行くで。切ったらこの知らせが行ったあとにアカウント作った人にもちゃんとお知らせが行くで。" + needConfirmationToRead: "既読にするんやったら確認してや" + needConfirmationToReadDescription: "オンにしたら、このお知らせを既読にする時に確認するで。ついでに、一括既読しても既読扱いにならへんで。" + end: "お知らせやめる" + tooManyActiveAnnouncementDescription: "お知らせが多すぎてUXが落ちそうや。終わったお知らせはアーカイブに突っ込んだほうがええかも。" readConfirmTitle: "既読にしてええんやな?" - readConfirmText: "「{title}」の内容を読み、既読にします。" + readConfirmText: "「{title}」はもう読んだから既読にするで。" shouldNotBeUsedToPresentPermanentInfo: "新規ユーザーのUXを損ねやすいから、お知らせはストック情報やのうてフロー情報の掲示に使った方がええで。" - dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が高くなるから、使用は慎重にすんのがおすすめやで。" + dialogAnnouncementUxWarn: "ダイアログ形式のお知らせがいっぺんに2コ以上ある場合、UXに良うないことが多いから、使うんは慎重にすんのがおすすめやで。" silence: "通知せんで" - silenceDescription: "オンにすると、このお知らせは通知されないで、既読にする必要もなくなるで。" + silenceDescription: "オンにすると、このお知らせは通知されへんし、既読にする必要もなくなるで。" _initialAccountSetting: accountCreated: "アカウント作り終わったで。" letsStartAccountSetup: "アカウントの初期設定をしよか。" - letsFillYourProfile: "最初はあんたのプロフィールを設定しよか。" + letsFillYourProfile: "最初はあんたのプロフィールを設定するで。" profileSetting: "プロフィール設定" privacySetting: "プライバシー設定" theseSettingsCanEditLater: "この設定はあとから変えれるで。" youCanEditMoreSettingsInSettingsPageLater: "これ以外にもいろんな設定を「設定」ページからできるで。後で確認してみてな。" followUsers: "タイムラインを構築するために、気になるユーザーをフォローしてみ。" pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をあんたのデバイスで受け取れるで。" - initialAccountSettingCompleted: "初期設定が終わったで。" + initialAccountSettingCompleted: "初期設定終わりや!" haveFun: "{name}、楽しんでな~" - youCanContinueTutorial: "このまま{name}(Misskey)の使い方のチュートリアルに進めるけど、ここで中断してすぐに使い始めることもできるで。" - startTutorial: "チュートリアルを開始するで" + youCanContinueTutorial: "こんまま{name}(Sharkey)の使い方のチュートリアルにも行けるけど、ここでやめてすぐに使い始めてもええで。" + startTutorial: "チュートリアルはじめる" skipAreYouSure: "初期設定飛ばすか?" laterAreYouSure: "初期設定あとでやり直すん?" _initialTutorial: - launchTutorial: "チュートリアルを見るで" + launchTutorial: "チュートリアル見るで" title: "チュートリアルやで" wellDone: "やるやん" - skipAreYouSure: "チュートリアルをやめるか?" + skipAreYouSure: "チュートリアルやめるか?" _landing: title: "チュートリアルによう来たな" - description: "ここでは、Misskeyの基本的な使い方や機能を確認できるで。" + description: "ここでは、Sharkeyのカンタンな使い方とか機能を確かめれんで。" _note: title: "ノートってなんや?" - description: "Misskeyでの投稿は「ノート」って呼ばれてるで。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されてるで。" - reply: "返信することもできるで。返信に対しての返信も可能で、スレッドのように会話を続けることもできるで。" - renote: "そのノートを自分のタイムラインに流して共有することもできるで。テキストを追加して引用することもできるで。" - reaction: "ツッコミをつけることもできるで。細かいことは次のページで解説するで。" + description: "Sharkeyでの投稿は「ノート」って呼ばれてんで。ノートは順々にタイムラインに載ってて、リアルタイムで新しくなってってんで。" + reply: "返信もできるで。返信の返信もできるから、スレッドっぽく会話をそのまま続けれもするで。" + renote: "そのノートを自分のタイムラインに流して共有できるで。テキスト入れて引用してもええな。" + reaction: "ツッコミをつけることもできるで。細かいことは次のページや。" + menu: "ノートの詳細を出したり、リンクをコピーしたり、いろいろできんねん。" + _reaction: + title: "ツッコミってなんや?" + description: "ノートには「ツッコミ」できんねん。「いいね」とか何言っとるかわからんし、簡単に表現できるのはええことやん?" + letsTryReacting: "ノートの「+」ボタンでツッコめるわ。試しに下のノートにツッコんでみ。" + reactToContinue: "ツッコんだら進めるようになるで。" + reactNotification: "あんたのノートが誰かにツッコまれたら、すぐ通知するで。" + reactDone: "「ー」ボタンでツッコミやめれるで。" + _timeline: + title: "タイムラインのしくみ" + description1: "Sharkeyには、いろいろタイムラインがあんで(ただ、サーバーによっては無効化されてるところもあるな)。" + home: "あんたがフォローしてるアカウントの投稿が見れんねん。" + local: "このサーバーの中におる全員の投稿が見れるで。" + social: "ホームタイムラインの投稿もローカルタイムラインのも一緒に見れるで。" + global: "繋がってる他の全サーバーからの投稿が見れるで。" + description2: "それぞれのタイムラインは、いつでも画面上で切り替えられんねん。覚えとき。" + description3: "その他にも、リストタイムラインとかチャンネルタイムラインとかがあんねん。詳しいのは{link}を見とき。" + _postNote: + title: "ノートの投稿設定" + description1: "Sharkeyにノートを投稿するとき、いろんなオプションが付けれるで。投稿画面はこんな感じや。" + _visibility: + description: "ノートを見れる相手を制限できるわ。" + public: "みんなに見せるで。" + home: "ホームタイムラインにだけ見せるで。フォロワーとか、プロフィールを見に来た人、リノートからも見れるから、実質は全員見れるけどな。あんまし広がりにくいってことや。" + followers: "フォロワーにだけ見せるで。自分以外はリノートできへんし、フォロワー以外は絶対に見れへん。" + direct: "指定した人にだけ公開されて、ついでに通知も送るで。ダイレクトメールの代わりとして使ってな。" + doNotSendConfidencialOnDirect1: "機密情報を送るときは十分注意せえよ。" + doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容が見れるから、信用できへんサーバーのひとにダイレクト投稿するときには、めっちゃ用心しとくんやで。" + localOnly: "他のサーバーに投稿せえへんくなるで。他の公開範囲とか一切無視して、他のサーバーの人らはこの設定がされたノートは絶対に見れへん。" + _cw: + title: "内容隠し(CW)" + description: "本文のかわりに「注釈」に書いた内容だけ見せるで。「続き見して!」を押すと本文も見れんねん。" + _exampleNote: + cw: "飯テロ注意" + note: "チョコドーナツめっちゃ美味かったわ🍩😋" + useCases: "サーバーのガイドラインに決められとるノートに使うたり、ネタバレとかきわどい内容を自分で隠したりするとき用やな。" + _howToMakeAttachmentsSensitive: + title: "のっけたファイルをセンシティブにするんは?" + description: "サーバーのガイドラインに書いてあったり、そのままおっぴろげとくのはあんま良うないファイルには「センシティブ」っちゅう設定をつけるんや。" + tryThisFile: "試しに、これにのっけてある画像をセンシティブにしてみいや!" + _exampleNote: + note: "納豆のフタ開けるときにやらかしてもうた…" + method: "のっけたファイルをセンシティブにするときは、そのファイルを押してメニュー開けて、「ちょっとこれはアカン」を押すんよ。" + sensitiveSucceeded: "ファイルをのっけるときは、サーバーの言うこと聞いてちゃんと設定するんやで。" + doItToContinue: "画像をちゃんと設定したら先に進めるで。" + _done: + title: "チュートリアル終わり!おつかれさん🎉" + description: "ここで紹介したのは全部の中のちょび~っとだけや。もっと使い方知りたいんやったら、{link}を見ときや。" +_timelineDescription: + home: "ホームタイムラインは、あんたがフォローしとるアカウントの投稿だけ見れるで。" + local: "ローカルタイムラインは、このサーバーにおる全員の投稿を見れるで。" + social: "ソーシャルタイムラインは、ホームタイムラインの投稿もローカルタイムラインのも一緒に見れるで。" + global: "グローバルタイムラインは、繋がっとる他のサーバーの投稿、全部ひっくるめて見れんで。" _serverRules: - description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。" + description: "新規登録前に見せる、サーバーのカンタンなルールを決めるで。内容は使うための決め事の要約がええと思うわ。" _serverSettings: iconUrl: "アイコン画像のURL" appIconDescription: "{host}がアプリとして表示してるんやつをアイコンを指定すんで。" - appIconUsageExample: "PWAや、スマートフォンのホーム画面にブックマークとして追加された時など" - appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるさかいに、塗り潰された余白のある背景があるものが推奨されるで。" - appIconResolutionMustBe: "解像度は必ず{resolution}である必要があるで。" + appIconUsageExample: "例えば、PWAとか、スマホのホームにブックマークしたときとか" + appIconStyleRecommendation: "円か角丸に切り取られることがあるさかい、塗り潰した余白のある背景があるものがおすすめや。" + appIconResolutionMustBe: "解像度は絶対{resolution}じゃないとアカン。" manifestJsonOverride: "manifest.jsonのオーバーライド" shortName: "略称" - shortNameDescription: "サーバーの名前が長い時に、代わりに表示することのできるあだ名。" - fanoutTimelineDescription: "" + shortNameDescription: "サーバーの名前が長ったらしい時に、代わりに出すあだ名。" + fanoutTimelineDescription: "入れると、おのおのタイムラインを取得するときにめちゃめちゃ動きが良うなって、データベースが軽くなるわ。でも、Redisのメモリ使う量が増えるから注意な。サーバーのメモリが足りんときとか、動きが変なときは切れるで。" + fanoutTimelineDbFallback: "データベースにフォールバックする" + fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。" _accountMigration: moveFrom: "別のアカウントからこのアカウントに引っ越す" moveFromSub: "別のアカウントへエイリアスを作る" - moveFromLabel: "引っ越し元のアカウント:" - moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したかったら、ここでエイリアスを作っとく必要があるで。必ずお引っ越しを実行する前に作っとかなあかんで!引っ越し元のアカウントをこんな風に入力してくれへんか?:@person@instance.com" + moveFromLabel: "引っ越しする前のアカウント #{n}" + moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引っ越ししたいんなら、ここでエイリアスを作っとかなアカンで。\n引っ越す前のアカウントをこんな感じに入力してや: @username@server.example.com\n入力欄空っぽやったら消しとくで(おすすめはせえへん)。" moveTo: "このアカウントをさらのアカウントに引っ越すで" - moveToLabel: "引っ越し先のアカウント:" - moveCannotBeUndone: "アカウントを移行すると、取り消すことはできへんくなります。" - moveAccountDescription: "おニューのアカウントに移行すんで。\n ・フォロワーがおニューの方を勝手にフォローすんで。\n ・このアカウントからのフォローはまるまる全部解除されんで。\n ・このアカウントでノート作れへんようになるで。\n\nフォロワーの移行は勝手にこっちでやっとくけど、フォローの移行は自分でしてや。移行前にこのアカウントでフォローエクスポートして、移行したあとすぐにおニューのところでインポートしてくれな。\nリストとかミュート、あとブロックもおんなじや。自分で移行してな。\n\n(この説明はこのサーバー、つまりSharkey v13.12.0から後の仕様や。Mastodonとか他のActivityPubソフトやとちょっと挙動が違うこともあんで。)" - moveAccountHowTo: "アカウントの引っ越しには、まず引っ越し先のアカウントで自分のアカウントに対しエイリアスを作成しなはれや。\nエイリアス作成した後、引っ越し先のアカウントを次のように入力してくれへんか?:@username@server.example.com" - startMigration: "引っ越しする" + moveToLabel: "引っ越し先のアカウント:" + moveCannotBeUndone: "アカウント引っ越したらもう戻せへん。" + moveAccountDescription: "おニューのアカウントに移行すんで。\n ・フォロワーがおニューの方を勝手にフォローすんで。\n ・このアカウントからのフォローはまるまる全部解除されんで。\n ・このアカウントでノート作れへんようになるで。\n\nフォロワーの移行は勝手にこっちでやっとくけど、フォローの移行は自分でしてや。移行前にこのアカウントでフォローエクスポートして、移行したあとすぐにおニューのところでインポートしてくれな。\nリストとかミュート、あとブロックもおんなじや。自分で移行してな。\n\n(この説明はこのサーバー、つまりMisskey v13.12.0から後の仕様や。Mastodonとか他のActivityPubソフトやとちょっと挙動が違うこともあんで。)" + moveAccountHowTo: "アカウントの引っ越しには、まず引っ越し先のアカウントで自分のアカウントに対しエイリアスを作ってな。\nエイリアス作ったら、引っ越し先のアカウントをこんな感じに入れてや: @username@server.example.com" + startMigration: "引っ越す" migrationConfirm: "ほんまにこのアカウントを {account} に引っ越すんか?一回引っ越してもうたら取り消されへんし、二度とこのアカウントを元に戻されへんくなるで。\nそれと、引っ越し先のアカウントでエイリアスが作れたかちゃ~んと確認しーや?" - movedAndCannotBeUndone: "\nアカウントはもう引っ越されてます。\n引っ越しを取り消すことはできまへん。" + movedAndCannotBeUndone: "\nアカウントはもう引っ越し済みや。\nこれはもう戻せへん。" postMigrationNote: "このアカウントからのフォロー解除は移行操作から丸一日経ったら実行されんで。\nこのアカウントのフォロー・フォロワー数はどっちも0や。フォローの解除はされへんから、あんたのフォロワーはこのアカウントのフォロワー向けの投稿をこの後も見れるで。" - movedTo: "引っ越し先のアカウント:" + movedTo: "引っ越し先のアカウント:" _achievements: earnedAt: "貰った日ぃ" _types: @@ -1253,10 +1329,10 @@ _achievements: title: "箕面の滝からノート" description: "ノートを5,000回投稿した" _notes10000: - title: "スーパーノート" + title: "えげつないノート" description: "ノートを10,000回投稿した" _notes20000: - title: "ニードモアノート" + title: "もっとノートよこせ!" description: "ノートを20,000回投稿した" _notes30000: title: "ノートノートノート" @@ -1353,7 +1429,7 @@ _achievements: title: "はじめてのフォロー" description: "初めてフォローした" _following10: - title: "ついてく、ついてく" + title: "すたこらさっさ" description: "フォローが10人超えた" _following50: title: "友達ぎょうさん" @@ -1406,10 +1482,10 @@ _achievements: description: "クライアント付けてから1時間経ってもうたで。" _noteDeletedWithin1min: title: "*おおっと*" - description: "投稿してから1分以内にその投稿を消した" + description: "投稿してから1分以内にその投稿をほかした" _postedAtLateNight: title: "夜行性" - description: "深夜にノートを投稿した" + description: "真夜中にノートを投稿した" flavor: "そろそろ寝よか" _postedAt0min0sec: title: "時報" @@ -1426,7 +1502,7 @@ _achievements: description: "サーバーのチャートを表示した" _outputHelloWorldOnScratchpad: title: "Hello, world!" - description: "スクラッチパッドで hello worldを出力した" + description: "スクラッチパッドで hello world を出力した" _open3windows: title: "マド開けすぎ" description: "ウィンドウを3つ以上開いた状態にした" @@ -1435,7 +1511,7 @@ _achievements: description: "ドライブのフォルダを再帰的な入れ子にしようとした" _reactWithoutRead: title: "ちゃんと読んだんか?" - description: "100文字以上のテキストを含むノートに投稿されてから3秒以内にツッコんだ" + description: "100文字以上のノートに投稿3秒以内にツッコんだ" _clickedClickHere: title: "ここをクリック" description: "ここをクリックした" @@ -1444,7 +1520,7 @@ _achievements: description: "10秒ごとに0.005%の確率で獲得" _setNameToSyuilo: title: "神様コンプレックス" - description: "名前を syuilo に設定した" + description: "名前を syuilo にした" _passedSinceAccountCreated1: title: "一周年" description: "アカウント作成から1年経過した" @@ -1470,8 +1546,11 @@ _achievements: description: "Brain Diverへのリンクを投稿したった" flavor: "Misskey-Misskey La-Tu-Ma" _smashTestNotificationButton: - title: "テスト過剰" - description: "通知テストをごく短時間のうちに連続して行ったねん" + title: "心配性" + description: "通知のテストしすぎやって" + _tutorialCompleted: + title: "Sharkeyひよっこ講座 修了証" + description: "チュートリアル全部やった" _role: new: "ロールの作成" edit: "ロールの編集" @@ -1482,60 +1561,63 @@ _role: assignTarget: "アサイン" descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれてるかを手動で管理するで。\nコンディショナルは条件を設定して、それに合うユーザーが自動で含まれるようになるで。" manual: "マニュアル" + manualRoles: "マニュアルロール" conditional: "コンディショナル" + conditionalRoles: "コンディショナルロール" condition: "条件" isConditionalRole: "これはコンディショナルロールやで" isPublic: "ロールを公開" - descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができるで。そんで、ユーザーのプロフィールでこのロールが表示されるで。" + descriptionOfIsPublic: "プロフィールでこのロールが出されるで。" options: "オプション" policies: "ポリシー" baseRole: "ベースロール" - useBaseValue: "ベースロールの値を使用" - chooseRoleToAssign: "アサインするロールを選択" + useBaseValue: "ベースロールの値使う" + chooseRoleToAssign: "アサインするロール選ぶ" iconUrl: "アイコン画像のURL" asBadge: "バッジとして見せる" descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。" - isExplorable: "ロールタイムラインを公開するで〜" - descriptionOfIsExplorable: "オンにしたらロールのタイムラインを公開するで〜。でもロールの公開をオフにしたら公開されへんよ。" + isExplorable: "ユーザーを見つけやすくしたる" + descriptionOfIsExplorable: "オンにしたらロールの面子一覧が「みつける」で公開されるし、ロールのタイムラインが使えるようになるで。" displayOrder: "表示順" descriptionOfDisplayOrder: "数がでかいほど、UI上で先に表示されるで。" - canEditMembersByModerator: "モデレーターのメンバー編集を許可" - descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。" + canEditMembersByModerator: "モデレーターがメンバーいじるのを許す" + descriptionOfCanEditMembersByModerator: "オンにすると、管理者だけやなくてモデレーターもこのロールにユーザーを入れたり抜いたりできるで。オフにすると管理者だけしかやれへんくなるで。" priority: "優先度" _priority: low: "低い" - middle: "中" + middle: "中くらい" high: "高い" _options: - gtlAvailable: "グローバルタイムラインの閲覧" - ltlAvailable: "ローカルタイムラインの閲覧" - canPublicNote: "パブリック投稿の許可" - canInvite: "サーバー招待コードの発行" - inviteLimit: "招待コードの作成可能数" - inviteLimitCycle: "招待コードの発行間隔" - inviteExpirationTime: "招待コードの有効期限" + gtlAvailable: "グローバルタイムライン見る" + ltlAvailable: "ローカルタイムライン見る" + canPublicNote: "パブリック投稿できるか" + canInvite: "サーバー招待コード作る" + inviteLimit: "招待コード作れる数" + inviteLimitCycle: "招待コードの作れる間隔" + inviteExpirationTime: "招待コードの期限" canManageCustomEmojis: "カスタム絵文字の管理" canManageAvatarDecorations: "アバターを飾るモンの管理" driveCapacity: "ドライブ容量" alwaysMarkNsfw: "勝手にファイルにNSFWをくっつける" - pinMax: "ノートのピン留めの最大数" - antennaMax: "アンテナの作成可能数" + pinMax: "ノートピン留めできる数" + antennaMax: "アンテナ作れる数" wordMuteMax: "ワードミュートの最大文字数" - webhookMax: "Webhockの作成可能数" - clipMax: "クリップの作成可能数" - noteEachClipsMax: "クリップ内のノートの最大数" - userListMax: "ユーザーリストの作成可能数" + webhookMax: "Webhook作れる数" + clipMax: "クリップ作れる数" + noteEachClipsMax: "クリップの中にノート作れる数" + userListMax: "ユーザーリスト作れる数" userEachUserListsMax: "ユーザーリスト内のユーザーの最大数" rateLimitFactor: "レートリミット" descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。" - canHideAds: "広告を表示させへん" - canSearchNotes: "ノート検索を使わすかどうか" - canUseTranslator: "翻訳機能の利用" + canHideAds: "広告映さへん" + canSearchNotes: "ノート探せるかどうか" + canUseTranslator: "翻訳使えるかどうか" + avatarDecorationLimit: "アイコンデコのいっちばんつけれる数" _condition: isLocal: "ローカルユーザー" isRemote: "リモートユーザー" - createdLessThan: "アカウント作成から~以内" - createdMoreThan: "アカウント作成から~経過" + createdLessThan: "アカウント作ってから~以内" + createdMoreThan: "アカウント作ってから~経過" followersLessThanOrEq: "フォロワー数が~以下" followersMoreThanOrEq: "フォロワー数が~以上" followingLessThanOrEq: "フォロー数が~以下" @@ -1544,45 +1626,45 @@ _role: notesMoreThanOrEq: "投稿を~以上しとる" and: "~かつ~" or: "~または~" - not: "~ではない" + not: "~じゃない" _sensitiveMediaDetection: - description: "機械学習を使って自動でセンシティブなメディアを検出して、モデレーションに役立てることができるで。サーバーの負荷が少し増えてまうなあ。" + description: "機械学習で自動できわどいメディアを検出して、運営しやすくするで。でもサーバーが少し重くなってまうわ。" sensitivity: "検出感度やで" sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減るで。感度を高くすると、検知漏れ(偽陰性)が減るで。" - setSensitiveFlagAutomatically: "NSFWフラグを設定するで" - setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されるで。" + setSensitiveFlagAutomatically: "センシティブフラグを設定するで" + setSensitiveFlagAutomaticallyDescription: "この設定切っても内部的には判定結果はそのままや。" analyzeVideos: "動画の解析をオンにするで" - analyzeVideosDescription: "画像に加えて動画も解析するようにするで。鯖の負荷が少し増えるで。" + analyzeVideosDescription: "画像だけじゃなくて動画も解析するようにするで。サーバーがちょっと重くなるで。" _emailUnavailable: - used: "もう使われとるで" + used: "もう使われとるわ" format: "形式がおかしいで" - disposable: "永久に使えるアドレスじゃないみたいやで" - mx: "正しいメールサーバーじゃない見たいやで" - smtp: "メールサーバーが応答してないみたいや" + disposable: "ずーっと使えるアドレスじゃないみたいや" + mx: "正しいメールサーバーじゃないっぽいわ" + smtp: "メールサーバーがうんともすんとも言わへん" _ffVisibility: public: "公開" followers: "フォロワーだけに公開" private: "非公開" _signup: - almostThere: "ほぼ完了やで" + almostThere: "ほぼ終わったようなもんや" emailAddressInfo: "あんたが使っとるメアドを入力してなー。入れたメアドが公開されることはないで。" - emailSent: "さっき入れたメールアドレス({email})宛に確認のメールが送られたで。メールに書かれたリンクにアクセスすれば、アカウントの作成が完了や!" + emailSent: "さっき入れたメアド({email})宛に確認メールを送ったで。メールに書かれたリンク押してアカウント作るの終わらしてな。\nメールの認証リンクの期限は30分や。" _accountDelete: accountDelete: "アカウントの削除" - mayTakeTime: "アカウントの削除は負荷がかかる処理やねんて。やから作ったコンテンツの数や上げたファイルの数が多いと削除が終わるまでに時間がかかることがあるんやって。" - sendEmail: "アカウントの削除が終わるときは、登録してたメールアドレス宛に通知を送るで。" - requestAccountDelete: "アカウント削除をリクエスト" + mayTakeTime: "アカウント消すんはサーバーが重いんやって。やから作ったコンテンツとか上げたファイルの数が多いと消し終わるまでに時間がかかるかもしれへん。" + sendEmail: "アカウントの消し終わるときは、登録してたメアドに通知するで。" + requestAccountDelete: "アカウント削除頼む" started: "削除処理が始まったで。" - inProgress: "削除が進んでるで" + inProgress: "今消しよるで" _ad: back: "戻る" - reduceFrequencyOfThisAd: "この広告の表示頻度を下げるで" + reduceFrequencyOfThisAd: "この広告ちょっとうざったらしいわ" hide: "表示せん" - timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されるで。" + timezoneinfo: "曜日はサーバーのタイムゾーンを元に決めるで。" adsSettings: "広告配信設定" notesPerOneAd: "リアタイ更新中に広告を出す間隔(ノートの個数な)" setZeroToDisable: "0でリアタイ更新時の広告配信を無効にすんで" - adsTooClose: "広告を出す間隔がめっちゃ短いから、ユーザー体験が著しく損なわれる可能性があんで。" + adsTooClose: "広告を出す間隔がめっちゃ短いから、ユーザー体験がめちゃめちゃ悪くなるかもしれへん。" _forgotPassword: enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。" ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。" @@ -1601,7 +1683,7 @@ _plugin: install: "プラグインのインストール" installWarn: "信頼できへんプラグインはインストールせんとってな" manage: "プラグインの管理" - viewSource: "ソースを表示" + viewSource: "ソース見る" _preferencesBackups: list: "作ったバックアップ" saveNew: "新しく保存" @@ -1642,24 +1724,25 @@ _displayOfSensitiveMedia: force: "常にメディアを隠すで" _instanceTicker: none: "表示せん" - remote: "リモートユーザーに表示" - always: "常に表示" + remote: "リモートユーザーに見せる" + always: "いつでも見せる" _serverDisconnectedBehavior: reload: "自動でリロード" dialog: "ダイアログで警告" quiet: "控えめに警告" _channel: - create: "チャンネルを作る" - edit: "チャンネルを編集" + create: "チャンネル作る" + edit: "チャンネルいじる" setBanner: "バナーを設定" removeBanner: "バナーを削除" featured: "トレンド" - owned: "管理中" + owned: "管理しとる" following: "フォロー中やで" - usersCount: "{n}人が参加中やで" + usersCount: "{n}人が参加しとる" notesCount: "{n}こ投稿があるで" nameAndDescription: "名前と説明" nameOnly: "名前だけ" + allowRenoteToExternal: "チャンネルの外にリノートできるようにする" _menuDisplay: sideFull: "横" sideIcon: "横(アイコン)" @@ -1685,7 +1768,7 @@ _theme: builtinThemes: "標準のテーマ" alreadyInstalled: "そのテーマはもうインストールされとるで?" invalid: "テーマの形式が間違ってるみたいや" - make: "テーマを作る" + make: "テーマ作る" base: "ベース" addConstant: "定数を追加" constant: "定数" @@ -1751,6 +1834,14 @@ _sfx: notification: "通知" antenna: "アンテナ受信" channel: "チャンネル通知" + reaction: "ツッコミ選んどるとき" +_soundSettings: + driveFile: "ドライブん中の音使う" + driveFileWarn: "ドライブん中のファイル選びや" + driveFileTypeWarn: "このファイルは対応しとらへん" + driveFileTypeWarnDescription: "音声ファイルを選びや" + driveFileDurationWarn: "音が長すぎるわ" + driveFileDurationWarnDescription: "長い音使うたらSharkey使うのに良うないかもしれへんで。それでもええか?" _ago: future: "未来" justNow: "ついさっき" @@ -1762,6 +1853,14 @@ _ago: monthsAgo: "{n}ヶ月前" yearsAgo: "{n}年前" invalid: "あらへん" +_timeIn: + seconds: "{n}秒後" + minutes: "{n}分後" + hours: "{n}時間後" + days: "{n}日後" + weeks: "{n}週間後" + months: "{n}ヶ月後" + years: "{n}年後" _time: second: "秒" minute: "分" @@ -1772,12 +1871,12 @@ _2fa: registerTOTP: "認証アプリの設定はじめる" step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。" step2: "次に、ここにあるQRコードをアプリでスキャンしてな~。" - step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。" + step2Click: "QRコード押したら、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。" step2Uri: "デスクトップアプリを使う時は次のURIを入れるで" step3Title: "確認コードを入れてーや" - step3: "アプリに表示されているトークンを入力して終わりや。" - setupCompleted: "設定が完了したで。" - step4: "これからログインするときも、同じようにトークンを入力するんやで" + step3: "アプリに映っとる確認コード(トークン)を入れて終わりや。" + setupCompleted: "設定が終わったで。" + step4: "これからログインするときも、同じようにコードを入れるんや。" securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。" registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーか端末の指紋認証やPINを使ってログインするように設定できるで。" @@ -1887,6 +1986,7 @@ _widgets: _userList: chooseList: "リストを選ぶ" clicker: "クリッカー" + birthdayFollowings: "今日誕生日のツレ" _cw: hide: "隠す" show: "続き見して!" @@ -1949,6 +2049,7 @@ _profile: changeAvatar: "アバター画像を変更するで" changeBanner: "バナー画像を変更するで" verifiedLinkDescription: "内容をURLに設定すると、リンク先のwebサイトに自分のプロフのリンクが含まれてる場合に所有者確認済みアイコンを表示させることができるで。" + avatarDecorationMax: "最大{max}つまでデコつけれんで" _exportOrImport: allNotes: "全てのノート" favoritedNotes: "お気に入りにしたノート" @@ -2063,7 +2164,7 @@ _notification: youGotMention: "{name}からのメンション" youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" - youRenoted: "{name}がRenoteしたみたいやで" + youRenoted: "{name}がリノートしたみたいやで" youWereFollowed: "フォローされたで" youReceivedFollowRequest: "フォロー許可してほしいみたいやな" yourFollowRequestAccepted: "フォローさせてもろたで" @@ -2076,6 +2177,9 @@ _notification: checkNotificationBehavior: "通知の表示を確かめるで" sendTestNotification: "テスト通知を送信するで" notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで" + reactedBySomeUsers: "{n}人がツッコんだで" + renotedBySomeUsers: "{n}人がリノートしたで" + followedBySomeUsers: "{n}人にフォローされたで" _types: all: "すべて" note: "あんたらの新規投稿" @@ -2145,8 +2249,8 @@ _webhookSettings: followed: "フォローもらったとき~!" note: "ノートを投稿したとき~!" reply: "返信があるとき~!" - renote: "Renoteされるとき~!" - reaction: "ツッコミがあるとき~!" + renote: "リノートされるとき~!" + reaction: "ツッコまれたとき~!" mention: "メンションがあるとき~!" _moderationLogTypes: createRole: "ロールを追加すんで" @@ -2175,13 +2279,15 @@ _moderationLogTypes: markSensitiveDriveFile: "ファイルをセンシティブ付与" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" resolveAbuseReport: "苦情を解決" - createInvitation: "招待コードを作成" + createInvitation: "招待コード作る" createAd: "広告を作んで" deleteAd: "広告ほかす" updateAd: "広告を更新" createAvatarDecoration: "アイコンデコレーションを作成" updateAvatarDecoration: "アイコンデコレーションを更新" deleteAvatarDecoration: "アイコンデコレーションを削除" + unsetUserAvatar: "この子のアイコン元に戻す" + unsetUserBanner: "この子のバナー元に戻す" _fileViewer: title: "ファイルの詳しい情報" type: "ファイルの種類" @@ -2214,6 +2320,11 @@ _externalResourceInstaller: description: "" _failedToFetch: title: "" + fetchErrorDescription: "他のサイトに繋がらんかったわ。もっかいやってもダメやったら、サイトの管理してる人に言っといて。" + parseErrorDescription: "他のサイトから持ってきたデータ、よう分からんかったわ。サイトの管理してる人に言っといて。" + _hashUnmatched: + title: "ちゃんとしたデータが持ってこれんかったわ" + description: "もらったデータがなんかおかしいっぽいわ。ちょっと危ないからインストールはできへん。サイト管理してる人に言っといてな。" _pluginParseFailed: title: "AiScriptエラー起こしてもうたねん" description: "データは取得できたものの、AiScript解析時にエラーがあったから読み込めへんかってん。すまんが、プラグインを作った人に問い合わせてくれへん?ごめんな。エラーの詳細はJavaScriptコンソール読んでな。" @@ -2222,7 +2333,20 @@ _externalResourceInstaller: description: "プラグインのインストール中に問題発生してもた、もう1度試してな。エラーの詳細はJavaScriptのコンソール見てや。" _themeParseFailed: title: "テーマ解析エラー" - description: "データは取得できたものの、テーマファイル解析時にエラーがあったから読み込めへんかってん。すまんが、テーマ作った人に問い合わせてくれへん?ごめんな。エラーの詳細はJavaScriptコンソール読んでな。" + description: "データは取れたんやが、テーマファイル読み込んどる時にエラーがあったから読み込めへんかったわ。すまんけど、テーマ作った人に言うてくれへん?ごめんな。エラーの詳細はJavaScriptコンソール読んでな。" _themeInstallFailed: title: "テーマインストールに失敗してもた" - description: "テーマのインストール中に問題発生してもた、もう1度試してな。エラーの詳細はJavaScriptのコンソール見てや。" + description: "なんかテーマインストールできんかったわ。もう一回試してな。細かいのはJavaScriptのコンソール見てや。" +_dataSaver: + _media: + title: "メディアの読み込み" + description: "絵・動画が自動で読まれるのをふせぐわ。隠れてる絵・動画はタップするとひょっこりはんしてくれんで。" + _avatar: + title: "アイコンの絵" + description: "アイコン画像のアニメが止まるで。普通の画像よりもデータ量がでかいから、もっと通信量を節約できるねん。" + _urlPreview: + title: "URLプレビューのサムネイル画像" + description: "URLプレビューのサムネイル画像が読み込まへんなるで。" + _code: + title: "コードハイライト" + description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。" diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml new file mode 100644 index 0000000000..566667ba79 --- /dev/null +++ b/locales/ko-GS.yml @@ -0,0 +1,723 @@ +--- +_lang_: "한국어(경상)" +headlineMisskey: "노트로 이언 네트워크" +introMisskey: "어서 오이소! Misskey넌 오픈소스 분산헹 마이크로 블로그 서비스입니다.\n‘노트’럴 맨걸어서 지검 일나넌 일얼 노누던가 내 이바구럴 남한데 서 보이소.📡\n‘리액션’ 기넝서 남으 노트에 억수로 빠리게 답할 수 잇십니다.👍\n새롭운 세게럴 탐험해 보입시다.🚀" +poweredByMisskeyDescription: "{name} 서버넌 오픈소스 플랫폼 Misskey으 서버 가운데 하나입니다." +monthAndDay: "{month}월 {day}일" +search: "찾기" +notifications: "알림" +username: "사용자 이럼" +password: "비밀번호" +forgotPassword: "비밀번호럴 잊엇뿟십니꺼?" +fetchingAsApObject: "연합서 찾아보고 잇어예" +ok: "예" +gotIt: "알것어예" +cancel: "아이예" +noThankYou: "뎃어예" +enterUsername: "사용자 이럼 서기" +renotedBy: "{user}님이 리노트햇어예" +noNotes: "노트가 없십니다" +noNotifications: "알림이 없십니다" +instance: "서버" +settings: "설정" +notificationSettings: "알림 설정" +basicSettings: "기본 설정" +otherSettings: "다린 설정" +openInWindow: "창서 옐기" +profile: "프로필" +timeline: "타임라인" +noAccountDescription: "자기소개가 없십니다" +login: "로그인" +loggingIn: "로그인하고 잇어예" +logout: "로그아웃" +signup: "가입하기" +uploading: "올리고 잇어예" +save: "저장하기" +users: "사용자" +addUser: "사용자 옇기" +favorite: "질겨찾기" +favorites: "질겨찾기" +unfavorite: "질겨찾기서 어ᇝ애기" +favorited: "질겨찾기에 담앗십니다." +alreadyFavorited: "벌시로 질겨찾기에 담기 잇십니다." +cantFavorite: "질겨찾기에 몬 담았십니다." +pin: "프로필에 붙이기" +unpin: "프로필서 띠기" +copyContent: "내용 복사하기" +copyLink: "링크 복사하기" +copyLinkRenote: "리노트 링크 복사" +delete: "내삐리기" +deleteAndEdit: "내삐리고 새로 적기" +deleteAndEditConfirm: "요 노트럴 뭉캐고 새로 적십니꺼? 요 노트서 리액션하고 리노트, 답하기도 말캉 뭉캐집니다." +addToList: "리스트에 옇기" +addToAntenna: "안테나에 옇기" +sendMessage: "메시지 보내기" +copyRSS: "알에스에스 복사하기" +copyUsername: "사용자 이럼 복사하기" +copyUserId: "사용자 아이디 복사하기" +copyNoteId: "노트 아이디 복사하기" +copyFileId: "파일 아이디 복사하기" +copyFolderId: "폴더 아이디 복사하기" +copyProfileUrl: "프로필 주소 복사하기" +searchUser: "사용자 찾기" +reply: "답하기" +loadMore: "더 볼래예" +showMore: "더 볼래예" +showLess: "꺼기" +youGotNewFollower: "새 팔로워가 잇십니다" +receiveFollowRequest: "팔로잉 요청이 잇십니다" +followRequestAccepted: "팔로잉이 받아딜이젓십니다" +mention: "멘션" +mentions: "받언 멘션" +directNotes: "쪽지 서기" +importAndExport: "가오기하고 내가기" +import: "가오기" +export: "내가기" +files: "파일" +download: "내리받기" +driveFileDeleteConfirm: "‘{name}’ 파일얼 뭉캡니꺼? 요 파일얼 서넌 콘텐츠도 뭉캐집니다." +unfollowConfirm: "{name}님얼 고마 팔로잉합니꺼?" +exportRequested: "내가기 요청얼 햇십니다. 시간이 쪼매 걸릴 깁니다. 요청이 껕나모 ‘드라이브’에 옇십니다." +importRequested: "가오기 요청얼 햇십니다. 시간이 쪼매 걸릴 깁니다." +lists: "리스트" +noLists: "리스트가 없십니다" +note: "노트" +notes: "노트" +following: "팔로잉" +followers: "팔로워" +followsYou: "내럴 팔로잉합니다" +createList: "리스트 맨걸기" +manageLists: "리스트 간리하기" +error: "우짭니꺼" +somethingHappened: "먼가 일낫십니다" +retry: "다시 하기" +pageLoadError: "하멘 부리오기가 아이뎁니다." +pageLoadErrorDescription: "네트워크나 브라우저 캐시 때문일 깁니다. 캐시럴 뭉캐던가 쪼매 잇다 새로 해 주이소." +serverIsDead: "서버가 대답얼 아이합니다. 쪼매 잇다 새로 해 주이소." +youShouldUpgradeClient: "요 하멘얼 볼라먼 새로 곤치던가 새 버전으 클라이언트럴 받아 서 보이소." +enterListName: "리스트 이럼 서기" +privacy: "개인 정보" +makeFollowManuallyApprove: "팔로잉얼 하나석 받아딜이기" +defaultNoteVisibility: "기본 공개 범위" +follow: "팔로우" +followRequest: "팔로우 요청하기" +followRequests: "팔로우 요청" +unfollow: "팔로우 무루기" +followRequestPending: "팔로우 수락 지둘림" +enterEmoji: "이모지 서기" +renote: "리노트" +unrenote: "리노트 무루기" +renoted: "리노트럴 햇십니다." +cantRenote: "요 걸언 리노트럴 몬 합니다." +cantReRenote: "리노트넌 지럴 리노트 몬 합니다." +quote: "따오기" +inChannelRenote: "채널 안 리노트" +inChannelQuote: "채널 안 따오기" +pinnedNote: "붙인 노트" +pinned: "프로필에 붙이기" +you: "나" +clickToShow: "누질라서 보기" +sensitive: "수ᇚ힛섭니다" +add: "옇기" +reaction: "반엉" +reactions: "반엉" +reactionSettingDescription2: "꺼시서 두고, 누질라서 뭉캐고, ‘+’럴 누질라서 옇십니다." +rememberNoteVisibility: "공개 범위럴 기억하기" +attachCancel: "붙임 빼기" +markAsSensitive: "수ᇚ힘 설정" +unmarkAsSensitive: "수ᇚ힘 무루기" +enterFileName: "파일 이럼 서기" +mute: "수ᇚ후기" +unmute: "수ᇚ훈 거 무루기" +renoteMute: "리노트 수ᇚ후기" +renoteUnmute: "리노트 수ᇚ훈 거 무루기" +block: "차단하기" +unblock: "차단 무루기" +suspend: "얼우기" +unsuspend: "얼우기 풀기" +blockConfirm: "차단합니꺼?" +unblockConfirm: "차단얼 무룹니꺼?" +suspendConfirm: "얼웁니꺼?" +unsuspendConfirm: "얼운 거 풉니꺼?" +selectList: "리스트 개리기" +editList: "리스트 적기" +selectChannel: "채널 개리기" +selectAntenna: "안테나 개리기" +editAntenna: "안테나 적기" +selectWidget: "위젯 개리기" +editWidgets: "위젯 적기" +editWidgetsExit: "고마 적기" +customEmojis: "사용자 지정 이모지" +emoji: "이모지" +emojis: "이모지" +emojiName: "이모지 이럼" +emojiUrl: "이모지 주소" +addEmoji: "이모지 옇기" +settingGuide: "개않언 설정" +cacheRemoteFiles: "웬겍 파일 캐시하기" +cacheRemoteFilesDescription: "요 설정얼 키모 웬겍 파일얼 요 서버으 스토리지에 캐시합니다. 미디어가 사게 비이지먼 서버으 스토리지럴 마이 섭니다. 웬겍 사용자가 얼매나 캐시럴 둘 긴가넌 고 옉할으 드라이브 크기 제한마중 다립니다. 요 제한얼 넘구모 엣날 파일버터 캐시서 뭉캐지서 링크가 뎁니다. 요 설정얼 꺼모 웬겍 파일언 첨버터 링크가 뎁니다. 이미지으 섬네일얼 맨걸던 사용자으 개인 정보럴 징키던 할라먼 default.yml서 proxyRemoteFiles럴 ture로 하입시다." +youCanCleanRemoteFilesCache: "파일 간리으 🗑️ 모냥얼 누질리모 캐시럴 말캉 뭉캘 수 잇십니다." +cacheRemoteSensitiveFiles: "웬겍으 수ᇚ힌 파일얼 캐시하기" +cacheRemoteSensitiveFilesDescription: "요 설정얼 꺼모 웬겍 수ᇚ힌 파일이 캐시하지 아이하고 바리 링크합니다." +flagAsBot: "자동 게정입니다" +flagAsBotDescription: "요 게정얼 프로그램서 설라먼 키야 합니다. 키모 다런 개발자가 반엉얼 끋없이 데풀이하지 몬 하게 도아 줄 수 잇고 Misskey으 시스템서 자동 게정이 뎁니다." +flagAsCat: "애웅애웅애웅애웅!" +flagAsCatDescription: "애옹?" +flagShowTimelineReplies: "타임라인서 노트으 답하기 보기" +flagShowTimelineRepliesDescription: "키모 타임라인서 다런 사용자덜으 답하기도 봅니다." +autoAcceptFollowed: "팔로잉하넌 사용자으 팔로잉 요청 바리 받아딜이기" +addAccount: "게정 옇기" +reloadAccountsList: "게정 리스트으 정보 새로 바꾸기" +loginFailed: "로그인이 아이뎁니다." +showOnRemote: "웬겍서 보기" +general: "일반" +wallpaper: "벡지" +setWallpaper: "벡지 설정" +removeWallpaper: "벡지 뭉캐기" +searchWith: "찾기: {q}" +youHaveNoLists: "리스트가 없십니다" +followConfirm: "{name}님얼 팔로잉합니꺼?" +proxyAccount: "프락시 게정" +proxyAccountDescription: "프락시 게정언 턱벨한 조겐서 웬겍 팔로잉얼 하넌 게정입니다. 사용자가 웬겍 사용자럴 리스트에 옇얼 때 리스트에 옇언 사용자럴 누도 팔로잉 아이하모 할동이 서버로 아이 오니께 요 게정이 아인 프락시 게정얼 팔로잉하게 합니다." +host: "호스트 이럼" +selectUser: "사용자 개리기" +recipient: "받넌 사람" +annotation: "주석" +federation: "옌합" +instances: "서버" +registeredAt: "첫 발겐" +latestRequestReceivedAt: "막죽에 받언 요청" +latestStatus: "막죽 상태" +storageUsage: "스토리지 사용량" +charts: "차트" +perHour: "한 시간마중" +perDay: "하리마중" +stopActivityDelivery: "할동 고마 보내기" +blockThisInstance: "요 서버 차단하기" +silenceThisInstance: "서버 수ᇚ후기" +operations: "동작" +software: "소프트웨어" +version: "버전" +metadata: "메타데이터" +withNFiles: "파일 {n}개" +monitor: "모니터" +jobQueue: "작업 대기옐" +cpuAndMemory: "시피유하고 메모리" +network: "네트워크" +disk: "디스크" +instanceInfo: "서버 정보" +statistics: "통게" +clearQueue: "대기옐 비우기" +clearQueueConfirmTitle: "대기옐얼 비웁니꺼?" +clearQueueConfirmText: "대기옐에 잇넌 걸얼 아이 보냅니다. 흐이 요 동작언 할 필요가 없십니다." +clearCachedFiles: "캐시 비우기" +clearCachedFilesConfirm: "캐시한 웬겍 파일얼 말캉 뭉캡니꺼?" +blockedInstances: "차단한 서버" +blockedInstancesDescription: "차단할라넌 서버으 호스트럴 줄 바꿈해서로 비이 줍니다. 차단한 서버넌 요 서버하고 교류 몬 합니다." +silencedInstances: "수ᇚ훈 서버" +silencedInstancesDescription: "수ᇚ훌라넌 서버으 호스트럴 줄 바꿈해서로 비이 줍니다. 수ᇚ훈 서버으 게정언 말캉 ‘수ᇚ후기’가 데서 팔로잉 요청만 데고 팔로워가 아인 로컬 게정서 멘션얼 몬 합니다. 차단한 서버넌 상간 없십니다." +muteAndBlock: "수ᇚ훔하고 차단" +mutedUsers: "수ᇚ훈 사용자" +blockedUsers: "차단한 사용자" +noUsers: "사용자가 없십니다" +editProfile: "프로필 적기" +noteDeleteConfirm: "요 노트럴 뭉캡니꺼?" +pinLimitExceeded: "더 몬 붙입니다" +intro: "Misskey럴 다 깔앗십니다! 간리자 게정얼 맨걸어 보입시다." +done: "햇어예" +processing: "처리하고 잇어예" +preview: "미리보기" +default: "기본값" +defaultValueIs: "기본값: {value}" +noCustomEmojis: "이모지가 없십니다" +noJobs: "작업이 없십니다" +federating: "옌합하고 잇어예" +blocked: "차단햇어예" +suspended: "고만 보내예" +all: "말캉" +subscribing: "구독하고 잇어예" +publishing: "보내고 잇어예" +notResponding: "답이 없어예" +instanceFollowing: "서버으 팔로잉" +instanceFollowers: "서버으 팔로워" +instanceUsers: "서버으 사용자" +changePassword: "비밀번호 바꾸기" +security: "보안" +retypedNotMatch: "선 거가 안 맞십니다." +currentPassword: "지검 비밀번호" +newPassword: "새 비밀번호" +newPasswordRetype: "새 비밀번호 다시 서기" +attachFile: "파일 붙이기" +more: "더 볼래예!" +featured: "인기" +usernameOrUserId: "사용자 이럼이나 사용자 아이디" +noSuchUser: "사용자럴 몬 찾앗십니다" +lookup: "찾아보기" +announcements: "공지 걸" +imageUrl: "이미지 주소" +remove: "내삐리기" +removed: "뭉캣십니다" +removeAreYouSure: "‘{x}’(얼)럴 뭉캡니꺼?" +deleteAreYouSure: "‘{x}’(얼)럴 뭉캡니꺼?" +resetAreYouSure: "아시로 데돌립니꺼?" +areYouSure: "갠찮십니꺼?" +saved: "저장햇십니다" +messaging: "대화" +upload: "올리기" +keepOriginalUploading: "온본 두기" +keepOriginalUploadingDescription: "이미지럴 올릴 때 온본얼 고대로 둡니다. 꺼모 올릴 때 브라우저서 웹 공개 이미지럴 맨겁니다." +fromDrive: "드라이브서" +fromUrl: "주소서" +uploadFromUrl: "주소 올리기" +uploadFromUrlDescription: "올리기할라넌 파일으 주소" +uploadFromUrlRequested: "올리기럴 요청햇십니다" +uploadFromUrlMayTakeTime: "올리기가 껕날라먼 시간이 쪼매 걸릴 깁니다." +explore: "살펴보기" +messageRead: "이럿어예" +noMoreHistory: "요카마 엣날 기록이 없십니다" +startMessaging: "대화하기" +nUsersRead: "{n}멩이 이럿십니다" +agreeTo: "{0}에 동이하기" +agree: "동이합니다" +agreeBelow: "밑으 내용에 동이합니다" +basicNotesBeforeCreateAccount: "주이할 내용" +termsOfService: "이용 약간" +start: "시작하기" +home: "덜머리" +remoteUserCaution: "웬겍 사용자넌 정보가 학실하지 아이할 수 잇십니다." +activity: "할동" +images: "이미지" +image: "이미지" +birthday: "생일" +yearsOld: "{age}살" +registeredDate: "맨건 날" +location: "장소" +theme: "테마" +themeForLightMode: "볽엄 모드서 설 테마" +themeForDarkMode: "어덥엄 모드서 설 테마" +light: "볽엄" +dark: "어덥엄" +lightThemes: "볽언 테마" +darkThemes: "어덥언 테마" +syncDeviceDarkMode: "디바이스 쪽 어덥엄 모드하고 같구로 마추기" +drive: "드라이브" +fileName: "파일 이럼" +selectFile: "파일 개리기" +selectFiles: "파일 개리기" +selectFolder: "폴더 개리기" +selectFolders: "폴더 개리기" +renameFile: "파일 이럼 바꾸기" +folderName: "폴더 이럼" +createFolder: "폴더 맨걸기" +renameFolder: "폴더 이럼 바꾸기" +deleteFolder: "폴더 뭉캐기" +folder: "폴더" +addFile: "파일 옇기" +emptyDrive: "드라이브가 비잇십니다" +emptyFolder: "폴더가 비잇십니다" +unableToDelete: "몬 뭉캡니다" +inputNewFileName: "새 파일 이럼얼 서 보이소" +inputNewDescription: "새 설멩얼 서 보이소" +inputNewFolderName: "새 폴더 이럼얼 서 보이소" +circularReferenceFolder: "엚길 폴더으 아래 폴더입니다." +hasChildFilesOrFolders: "요 폴더넌 아이 비잇어니께 몬 뭉캡니다." +copyUrl: "주소 복사하기" +rename: "이럼 바꾸기" +avatar: "아바타" +banner: "배너" +displayOfSensitiveMedia: "수ᇚ힌 옝상물 보기" +whenServerDisconnected: "서버하고 옌겔이 껂기모" +disconnectedFromServer: "서버하고 옌겔이 껂깃십니다" +reload: "새로곤침" +doNothing: "무시하기" +reloadConfirm: "새로곤침합니꺼?" +watch: "간심 갖기" +unwatch: "간심 고마 갖기" +accept: "받기" +reject: "아이 받기" +normal: "일반" +instanceName: "서버 이럼" +instanceDescription: "서버 소개" +maintainerName: "간리자 이럼" +maintainerEmail: "간리자 전자우펜" +tosUrl: "이용 약간 주소" +thisYear: "올개" +thisMonth: "요달" +today: "오올" +dayX: "{day}일" +monthX: "{month}월" +yearX: "{year}년" +pages: "바닥" +integration: "옌겔" +connectService: "옌겔하기" +disconnectService: "껂기" +enableLocalTimeline: "로컬 타임라인 키기" +enableGlobalTimeline: "글로벌 타임라인 키기" +disablingTimelinesInfo: "요 타임라인얼 꺼도 간리자하고 중재자넌 고대로 설 수 잇십니다." +registration: "맨걸기" +enableRegistration: "누라도 새로 맨걸 수 잇거로 하기" +invite: "초대하기" +driveCapacityPerLocalAccount: "로컬 사용자 하나마중 드라이브 커기" +driveCapacityPerRemoteAccount: "웬겍 사용자 하나마중 드라이브 커기" +inMb: "메가바이트 단이" +bannerUrl: "배너 이미지 주소" +backgroundImageUrl: "배겡 이미지 주소" +basicInfo: "기본 정보" +pinnedUsers: "붙인 사용자" +pinnedUsersDescription: "‘살펴보기’서 붙일라넌 사용자럴 줄 바꿈해서로 적십니다." +pinnedPages: "붙인 바닥" +pinnedPagesDescription: "서버으 대문서 붙일라넌 바닥으 겡로럴 줄 바꿈해서로 적십니다." +pinnedClipId: "붙일 클립으 아이디" +pinnedNotes: "붙인 노트" +hcaptcha: "에이치캡차" +enableHcaptcha: "에이치캡차 키기" +hcaptchaSiteKey: "사이트키" +hcaptchaSecretKey: "시크릿키" +recaptcha: "리캡차" +enableRecaptcha: "리캡차 키기" +recaptchaSiteKey: "사이트키" +recaptchaSecretKey: "시크릿키" +turnstile: "턴스타일" +enableTurnstile: "턴스타일 키기" +turnstileSiteKey: "사이트키" +turnstileSecretKey: "시크릿키" +avoidMultiCaptchaConfirm: "오만 캡차럴 서모 간섭이 잇얼 깁니다. 다린 캡차를 껍니꺼? ‘아이예’럴 누질리모 오만 캡차럴 키 둘 수도 잇십니다." +antennas: "안테나" +manageAntennas: "안테나 간리" +name: "이럼" +antennaSource: "받얼 소스" +antennaKeywords: "받얼 검색어" +antennaExcludeKeywords: "수ᇚ훌 검색어" +antennaKeywordsDescription: "띠어서기럴 하모 ‘거라고’가 데고 줄 바꿈얼 하모 ‘아이먼’이 뎁니다" +notifyAntenna: "새 노트럴 알리기" +withFileAntenna: "파일이 붙언 노트마" +enableServiceworker: "브라우저서 알림 포시럴 키기" +antennaUsersDescription: "사용자 이럼얼 줄 바꿈해서로 섭니다" +caseSensitive: "대소문자럴 구벨하기" +withReplies: "답하기도 옇기" +connectedTo: "요 게정하고 옌겔데어 잇십니다" +notesAndReplies: "걸하고 답걸" +withFiles: "파일에 붙이기" +silence: "수ᇚ후기" +silenceConfirm: "수ᇚ훕니꺼?" +unsilence: "수ᇚ후기 어ᇝ애기" +unsilenceConfirm: "수ᇚ후기럴 어ᇝ앱니꺼?" +popularUsers: "소문난 사용자" +recentlyUpdatedUsers: "얼마 전에 걸 선 사용자" +recentlyRegisteredUsers: "얼마 전에 맨건 사용자" +recentlyDiscoveredUsers: "얼마 전에 찾언 사용자" +exploreUsersCount: "사용자 {count}멩이 잇십니다." +exploreFediverse: "옌합우주 탐험하기" +popularTags: "소문난 태그" +userList: "리스트" +about: "정보" +aboutMisskey: "Misskey넌예" +administrator: "간리자" +token: "학인 기호" +2fa: "두 단게 정멩" +setupOf2fa: "두 단게 정멩 설정" +totp: "정멩 앱" +totpDescription: "정멩 앱서 단헤용 비밀번호 서기" +moderator: "중재자" +moderation: "중재" +moderationNote: "중재 노트" +addModerationNote: "중재 노트 옇기" +moderationLogs: "중재 일지" +nUsersMentioned: "{n}멩이 이바구하고 잇어예" +securityKeyAndPasskey: "보안키·패스키" +securityKey: "보안키" +lastUsed: "마지막 쓰임" +lastUsedAt: "마지막 쓰임: {t}" +unregister: "맨걸기 무루기" +passwordLessLogin: "비밀번호 없시 로그인" +passwordLessLoginDescription: "비밀번호 말고 보안키나 패스키 같은 것만 써 가 로그인합니다." +resetPassword: "비밀번호 재설정" +newPasswordIs: "새 비밀번호는 \"{password}\" 입니다" +reduceUiAnimation: "화면 움직임 효과들을 수ᇚ후기" +share: "노누기" +notFound: "몬 찾앗십니다" +notFoundDescription: "고런 주소로 들어가는 하멘은 없십니다." +uploadFolder: "기본 업로드 위치" +markAsReadAllNotifications: "모든 알림 이럿다고 표시" +markAsReadAllUnreadNotes: "모든 글 이럿다고 표시" +markAsReadAllTalkMessages: "모든 대화 이럿다고 표시" +help: "도움말" +inputMessageHere: "여따가 메시지를 입력해주이소" +close: "닫기" +invites: "초대하기" +members: "멤버" +transfer: "양도" +title: "제목" +text: "글" +enable: "키기" +next: "다음" +retype: "다시 서기" +noteOf: "{user}님으 노트" +quoteAttached: "따옴" +quoteQuestion: "따와가 작성하겠십니까?" +noMessagesYet: "아직 대화가 없십니다" +newMessageExists: "새 메시지가 있십니다" +onlyOneFileCanBeAttached: "메시지엔 파일 하나까제밖에 몬 넣십니다" +invitations: "초대하기" +invitationCode: "초대장" +checking: "학인하고 잇십니다" +passwordMatched: "맞십니다" +passwordNotMatched: "안 맞십니다" +signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소." +or: "아니면" +language: "언어" +uiLanguage: "UI 표시 언어" +aboutX: "{x}에 대해서" +emojiStyle: "이모지 모양" +native: "기본" +disableDrawer: "드로어 메뉴 쓰지 않기" +showNoteActionsOnlyHover: "마우스 올맀을 때만 노트 액션 버턴 보이기" +noHistory: "기록이 없십니다" +signinHistory: "로그인 기록" +enableAdvancedMfm: "복잡한 MFM 키기" +enableAnimatedMfm: "정신사나운 MFM 키기" +doing: "잠만예" +category: "카테고리" +tags: "태그" +docSource: "요 문서의 원본" +createAccount: "게정 맨걸기" +existingAccount: "원래 게정" +regenerate: "엎고 다시 맨걸기" +fontSize: "글자 크기" +mediaListWithOneImageAppearance: "사진 하나짜리 미디어 목록의 높이" +limitTo: "{x}로 제한" +noFollowRequests: "지둘리는 팔로우 요청이 없십니다" +openImageInNewTab: "새 탭서 사진 열기" +dashboard: "대시보드" +local: "로컬" +remote: "웬겍" +total: "합계" +weekOverWeekChanges: "저번주보다" +dayOverDayChanges: "어제보다" +appearance: "모냥" +clientSettings: "클라이언트 설정" +accountSettings: "게정 설정" +promotion: "선전" +promote: "선전하기" +numberOfDays: "며칠동안" +hideThisNote: "요 노트를 수ᇚ후기" +showFeaturedNotesInTimeline: "타임라인에다 추천 노트 보이기" +objectStorage: "오브젝트 스토리지" +useObjectStorage: "오브젝트 스토리지 키기" +objectStorageBaseUrl: "Base URL" +objectStorageBaseUrlDesc: "오브젝트 (미디어) 참조 링크 만들 때 쓰는 URL임다. CDN 내지 프락시를 쓴다 카멘은 그 URL을 갖다 늫고, 아이면 써먹을 서비스네 가이드를 봐봐가 공개적으로 접근할 수 있는 주소를 여 넣어 주이소. 그니께, 내가 AWS S3을 쓴다 카면은 'https://.s3.amazonaws.com', GCS를 쓴다 카면 'https://storage.googleapis.com/' 처럼 쓰믄 되입니더." +objectStorageBucket: "Bucket" +objectStorageBucketDesc: "써먹을 서비스의 바께쓰 이름을 여 써 주이소." +objectStoragePrefix: "Prefix" +objectStoragePrefixDesc: "요 Prefix 디렉토리 안에다가 파일이 들어감다." +objectStorageEndpoint: "Endpoint" +objectStorageEndpointDesc: "AWS S3을 쓸라멘 요는 비워두고, 아이멘은 그 서비스 가이드에 맞게 endpoint를 넣어 주이소. '' 내지 ':'처럼 넣십니다." +objectStorageRegion: "Region" +objectStorageRegionDesc: "'xx-east-1' 같은 region 이름을 옇어 주이소. 써먹을 서비스에 region 개념 같은 게 읎다! 카면은 대신에 'us-east-1'을 옇어 놓으이소. AWS 설정 파일이나 환경 변수를 갖다 끌어다 쓸 거면은 요는 비워 두이소." +objectStorageUseSSL: "SSL 쓰기" +objectStorageUseSSLDesc: "API 호출할 때 HTTPS 안 쓸거면은 꺼 두이소" +objectStorageUseProxy: "연결에 프락시 사용" +objectStorageUseProxyDesc: "오브젝트 스토리지 API 호출에 프락시 안 쓸 거면 꺼 두이소" +objectStorageSetPublicRead: "업로드할 때 'public-read' 설정하기" +s3ForcePathStyleDesc: "s3ForcePathStyle을 키면, 바께쓰 이름을 URL의 호스트명 말고 경로의 일부로써 취급합니다. 셀프 호스트 Minio 같은 걸 굴릴라믄 켜놔야 될 수도 있십니다." +serverLogs: "서버 로그" +deleteAll: "말캉 뭉캐기" +showFixedPostForm: "타임라인 우에 글 작성 칸 박기" +showFixedPostFormInChannel: "채널 타임라인 우에 글 작성 칸 박기" +withRepliesByDefaultForNewlyFollowed: "팔로우 할 때 기본적으로 답걸도 타임라인에 나오게 하기" +newNoteRecived: "새 노트 있어예" +sounds: "소리" +sound: "소리" +listen: "듣기" +none: "없음" +showInPage: "바닥서 보기" +popout: "새 창 열기" +volume: "음량" +masterVolume: "대빵 음량" +notUseSound: "음소거하기" +useSoundOnlyWhenActive: "Misskey가 활성화되어 있을 때만 소리 내기" +details: "좀 더" +chooseEmoji: "이모지 선택" +unableToProcess: "작업 다 몬 했십니다" +recentUsed: "최근 쓴 놈" +install: "설치" +uninstall: "삭제" +installedApps: "설치된 애플리케이션" +nothing: "뭣도 없어예" +installedDate: "설치한 날" +lastUsedDate: "마지막 사용" +state: "상태" +sort: "정렬하기" +ascendingOrder: "작은 순" +descendingOrder: "큰 순" +scratchpad: "스크래치 패드" +scratchpadDescription: "스크래치 패드는 AiScript를 끼적거리는 창입니더. Misskey랑 갖다 이리저리 상호작용하는 코드를 서가 굴리멘은 그 결과도 바로 확인할 수 있십니다." +output: "출력" +script: "스크립트" +disablePagesScript: "온갖 바닥서 AiScript를 쓰지 않음" +updateRemoteUser: "원겍 사용자 근황 알아오기" +unsetUserAvatar: "아바타 치우기" +unsetUserAvatarConfirm: "아바타 갖다 치울까예?" +unsetUserBanner: "배너 치우기" +unsetUserBannerConfirm: "배너 갖다 치울까예?" +deleteAllFiles: "파일 말캉 뭉캐기" +deleteAllFilesConfirm: "파일을 싸그리 다 뭉캐삐릴까예?" +removeAllFollowing: "팔로잉 말캉 무루기" +removeAllFollowingDescription: "{host} 서버랑 걸어놓은 모든 팔로잉을 무룹니다. 고 서버가 아예 없어지삐맀든가, 그런 경우에 하이소." +userSuspended: "요 게정은... 얼어 있십니다." +userSilenced: "요 게정은... 수ᇚ혀 있십니다." +relays: "릴레이" +addRelay: "릴레이 옇기" +addedRelays: "옇은 릴레이" +enableInfiniteScroll: "알아서 더 보기" +author: "맨던 사람" +manage: "간리" +emailServer: "전자우펜 서버" +email: "전자우펜" +emailAddress: "전자우펜 주소" +smtpHost: "호스트 이럼" +smtpPort: "포트" +smtpUser: "사용자 이럼" +smtpPass: "비밀번호" +display: "보기" +create: "맨걸기" +abuseReports: "신고하기" +reportAbuse: "신고하기" +reportAbuseRenote: "리노트 신고하기" +reportAbuseOf: "{name}님얼 신고하기" +reporter: "신고한 사람" +reporteeOrigin: "신고덴 사람" +reporterOrigin: "신고한 곳" +forwardReport: "웬겍 서버에 신고 보내기" +random: "무작이" +system: "시스템" +clip: "클립 맨걸기" +createNew: "새로 맨걸기" +notesCount: "노트 수" +renotesCount: "리노트한 수" +renotedCount: "리노트덴 수" +followingCount: "팔로우 수" +followersCount: "팔로워 수" +clips: "클립 맨걸기" +clearCache: "캐시 비우기" +unlikeConfirm: "좋네예럴 무룹니꺼?" +info: "정보" +user: "사용자" +administration: "간리" +on: "킴" +off: "껌" +clickToFinishEmailVerification: "[{ok}]럴 누질라서 전자우펜 정멩얼 껕내이소." +searchByGoogle: "찾기" +tenMinutes: "십 분" +oneHour: "한 시간" +oneDay: "하리" +oneWeek: "한 주" +oneMonth: "한 달" +file: "파일" +tools: "도구" +like: "좋네예!" +unlike: "좋네예 무루기" +numberOfLikes: "좋네예 수" +show: "보기" +roles: "옉할" +role: "옉할" +noRole: "옉할이 없십니다" +thisPostMayBeAnnoyingCancel: "아이예" +likeOnly: "좋네예마" +icon: "아바타" +replies: "답하기" +renotes: "리노트" +_initialAccountSetting: + startTutorial: "길라잡이 하기" +_initialTutorial: + launchTutorial: "길라잡이 보기" + title: "길라잡이" + skipAreYouSure: "길라잡이럴 껕냅니까?" + _landing: + title: "길라잡이에 어서 오이소" + _done: + title: "길라잡이가 껕낫십니다!🎉" +_achievements: + _types: + _tutorialCompleted: + description: "길라잡이럴 껕냇십니다" +_gallery: + liked: "좋네예한 걸" + like: "좋네예!" + unlike: "좋네예 무루기" +_email: + _follow: + title: "새 팔로워가 잇십니다" +_serverDisconnectedBehavior: + reload: "알아서 새로곤침" +_channel: + removeBanner: "배너 뭉캐기" +_theme: + keys: + mention: "멘션" +_sfx: + note: "새 노트" + notification: "알림" +_2fa: + step3Title: "학인 기호럴 서기" + renewTOTPCancel: "뎃어예" +_widgets: + profile: "프로필" + instanceInfo: "서버 정보" + notifications: "알림" + timeline: "타임라인" + activity: "할동" + federation: "옌합" + jobQueue: "작업 대기옐" + _userList: + chooseList: "리스트 개리기" +_cw: + show: "더 볼래예" +_visibility: + home: "덜머리" + followers: "팔로워" +_profile: + name: "이럼" + username: "사용자 이럼" +_exportOrImport: + followingList: "팔로잉" + muteList: "수ᇚ후기" + blockingList: "차단하기" + userLists: "리스트" +_charts: + federation: "옌합" +_timelines: + home: "덜머리" +_play: + script: "스크립트" +_pages: + like: "좋네예" + unlike: "좋네예 무루기" + blocks: + image: "이미지" + _note: + id: "노트 아이디" +_notification: + youWereFollowed: "새 팔로워가 잇십니다" + _types: + follow: "팔로잉" + mention: "멘션" + quote: "따오기" + reaction: "반엉" + _actions: + reply: "답하기" +_deck: + _columns: + notifications: "알림" + tl: "타임라인" + antenna: "안테나" + list: "리스트" + mentions: "받언 멘션" +_webhookSettings: + name: "이럼" +_moderationLogTypes: + suspend: "얼우기" + deleteNote: "노트 뭉캐기" + deleteUserAnnouncement: "사용자 공지 걸 뭉캐기" + resetPassword: "비밀번호 재설정" + resolveAbuseReport: "신고 해겔하기" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index ec346c9ec1..4a13012eed 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -2,14 +2,14 @@ _lang_: "한국어" headlineMisskey: "노트로 연결되는 네트워크" introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응을 추가할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀" -poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼Misskey를 사용한 서비스(Misskey 인스턴스라고 불립니다) 중 하나입니다." +poweredByMisskeyDescription: "{name} 서버는 오픈소스 플랫폼 Misskey의 서버 가운데 하나입니다." monthAndDay: "{month}월 {day}일" search: "검색" notifications: "알림" username: "유저명" password: "비밀번호" forgotPassword: "비밀번호 재설정" -fetchingAsApObject: "연합에 조회 중" +fetchingAsApObject: "연합에서 찾아보는 중" ok: "확인" gotIt: "알겠어요" cancel: "취소" @@ -45,7 +45,7 @@ pin: "프로필에 고정" unpin: "프로필에서 고정 해제" copyContent: "내용 복사" copyLink: "링크 복사" -copyLinkRenote: "Renote 링크 복사" +copyLinkRenote: "리노트 링크 복사" delete: "삭제" deleteAndEdit: "삭제 후 편집" deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니까? 이 노트에 대한 리액션, 리노트, 답글 또한 모두 삭제됩니다." @@ -53,8 +53,8 @@ addToList: "리스트에 추가" addToAntenna: "안테나에 추가" sendMessage: "메시지 보내기" copyRSS: "RSS 복사" -copyUsername: "유저명 복사" -copyUserId: "유저 ID 복사" +copyUsername: "사용자 이름 복사" +copyUserId: "사용자 ID 복사" copyNoteId: "노트 ID 복사" copyFileId: "파일 ID 복사" copyFolderId: "폴더 ID 복사" @@ -75,7 +75,7 @@ import: "가져오기" export: "내보내기" files: "파일" download: "다운로드" -driveFileDeleteConfirm: "파일 \"{name}\" 을 삭제하시겠습니까? 이 파일이 첨부된 노트도 함께 삭제됩니다." +driveFileDeleteConfirm: "‘{name}’ 파일을 삭제하시겠습니까? 이 파일을 사용하는 일부 콘텐츠도 삭제됩니다." unfollowConfirm: "{name}님을 언팔로우하시겠습니까?" exportRequested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 \"드라이브\"에 추가됩니다." importRequested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다." @@ -85,7 +85,7 @@ note: "노트" notes: "노트" following: "팔로잉" followers: "팔로워" -followsYou: "당신을 팔로우합니다" +followsYou: "나를 팔로우 합니다" createList: "리스트 만들기" manageLists: "리스트 관리" error: "오류" @@ -114,14 +114,19 @@ quote: "인용" inChannelRenote: "채널 내 리노트" inChannelQuote: "채널 내 인용" pinnedNote: "고정된 노트" -pinned: "프로필에 고정" -you: "당신" +pinned: "고정하기" +you: "나" clickToShow: "클릭하여 보기" sensitive: "열람 주의" add: "추가" reaction: "리액션" reactions: "리액션" -reactionSetting: "선택기에 표시할 리액션" +emojiPicker: "이모지 선택기" +pinnedEmojisForReactionSettingDescription: "리액션을 할 때 프로필에 고정하여 표시할 이모지를 설정할 수 있습니다" +pinnedEmojisSettingDescription: "이모지를 입력할 때 프로필에 고정하여 표시할 이모지를 설정할 수 있습니다" +emojiPickerDisplay: "선택기 표시" +overwriteFromPinnedEmojisForReaction: "리액션 설정을 덮어쓰기" +overwriteFromPinnedEmojis: "일반 설정을 덮어쓰기" reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다." rememberNoteVisibility: "공개 범위를 기억하기" attachCancel: "첨부 취소" @@ -130,7 +135,7 @@ unmarkAsSensitive: "열람주의 해제" enterFileName: "파일명을 입력" mute: "뮤트" unmute: "뮤트 해제" -renoteMute: "리노트를 뮤트" +renoteMute: "리노트 뮤트하기" renoteUnmute: "리노트 뮤트 해제" block: "차단" unblock: "차단 해제" @@ -247,13 +252,13 @@ security: "보안" retypedNotMatch: "입력이 일치하지 않습니다." currentPassword: "현재 비밀번호" newPassword: "새 비밀번호" -newPasswordRetype: "새 비밀번호 (재입력)" +newPasswordRetype: "새 비밀번호(재입력)" attachFile: "파일 첨부" -more: "더보기" -featured: "하이라이트" +more: "더 보기!" +featured: "유행" usernameOrUserId: "유저명이나 ID" noSuchUser: "유저를 찾을 수 없습니다" -lookup: "조회" +lookup: "찾아보기" announcements: "공지사항" imageUrl: "이미지 URL" remove: "삭제" @@ -261,6 +266,7 @@ removed: "삭제하였습니다" removeAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?" deleteAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?" resetAreYouSure: "초기화 하시겠습니까?" +areYouSure: "계속 진행하시겠습니까?" saved: "저장하였습니다" messaging: "대화" upload: "업로드" @@ -307,10 +313,11 @@ selectFiles: "파일 선택" selectFolder: "폴더 선택" selectFolders: "폴더 선택" renameFile: "파일 이름 변경" -folderName: "폴더명" +folderName: "폴더 이름" createFolder: "폴더 만들기" renameFolder: "폴더 이름 바꾸기" deleteFolder: "폴더 삭제" +folder: "폴더" addFile: "파일 추가" emptyDrive: "드라이브가 비어 있습니다" emptyFolder: "폴더가 비어 있습니다" @@ -330,10 +337,10 @@ disconnectedFromServer: "서버와의 연결이 끊어졌습니다" reload: "새로고침" doNothing: "무시하기" reloadConfirm: "새로고침 하시겠습니까?" -watch: "지켜보기" -unwatch: "지켜보기 해제" -accept: "허가" -reject: "거부" +watch: "관심 갖기" +unwatch: "관심 해제하기" +accept: "수락하기" +reject: "거절하기" normal: "일반" instanceName: "서버 이름" instanceDescription: "서버 소개" @@ -341,7 +348,7 @@ maintainerName: "관리자 이름" maintainerEmail: "관리자 이메일" tosUrl: "이용약관 URL" thisYear: "올해" -thisMonth: "이번 달" +thisMonth: "이달" today: "오늘" dayX: "{day}일" monthX: "{month}월" @@ -385,8 +392,8 @@ antennas: "안테나" manageAntennas: "안테나 관리" name: "이름" antennaSource: "받을 소스" -antennaKeywords: "받을 키워드" -antennaExcludeKeywords: "제외할 키워드" +antennaKeywords: "받을 검색어" +antennaExcludeKeywords: "제외할 검색어" antennaKeywordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다" notifyAntenna: "새로운 노트를 알림" withFileAntenna: "파일이 첨부된 노트만" @@ -418,9 +425,9 @@ setupOf2fa: "2단계 인증 설정" totp: "인증 앱" totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력" moderator: "모더레이터" -moderation: "모더레이션" -moderationNote: "모더레이션 노트" -addModerationNote: "모더레이션 노트 추가하기" +moderation: "조정" +moderationNote: "조정 기록" +addModerationNote: "조정 기록 추가하기" moderationLogs: "모더레이션 로그" nUsersMentioned: "{n}명이 언급함" securityKeyAndPasskey: "보안 키 또는 패스 키" @@ -437,7 +444,6 @@ share: "공유" notFound: "찾을 수 없습니다" notFoundDescription: "지정한 URL에 해당하는 페이지가 존재하지 않습니다." uploadFolder: "기본 업로드 위치" -cacheClear: "캐시 지우기" markAsReadAllNotifications: "모든 알림을 읽은 상태로 표시" markAsReadAllUnreadNotes: "모든 글을 읽은 상태로 표시" markAsReadAllTalkMessages: "모든 대화를 읽은 상태로 표시" @@ -479,7 +485,7 @@ language: "언어" uiLanguage: "UI 표시 언어" aboutX: "{x}에 대하여" emojiStyle: "이모지 스타일" -native: "네이티브" +native: "기본" disableDrawer: "드로어 메뉴를 사용하지 않기" showNoteActionsOnlyHover: "노트 액션 버튼을 마우스를 올렸을 때에만 표시" noHistory: "기록이 없습니다" @@ -507,7 +513,7 @@ dayOverDayChanges: "어제보다" appearance: "모양" clientSettings: "클라이언트 설정" accountSettings: "계정 설정" -promotion: "프로모션" +promotion: "홍보" promote: "프로모션하기" numberOfDays: "며칠동안" hideThisNote: "이 노트를 숨기기" @@ -544,6 +550,8 @@ showInPage: "페이지로 보기" popout: "새 창으로 열기" volume: "음량" masterVolume: "마스터 볼륨" +notUseSound: "음소거 하기" +useSoundOnlyWhenActive: "Misskey가 활성화 되어져 있을 때만 소리 출력하기" details: "자세히" chooseEmoji: "이모지 선택" unableToProcess: "작업을 완료할 수 없습니다" @@ -564,10 +572,14 @@ output: "출력" script: "스크립트" disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음" updateRemoteUser: "리모트 유저 정보 갱신" +unsetUserAvatar: "아바타 제거" +unsetUserAvatarConfirm: "아바타를 제거할까요?" +unsetUserBanner: "배너 제거" +unsetUserBannerConfirm: "배너를 제거할까요?" deleteAllFiles: "모든 파일 삭제" deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?" removeAllFollowing: "모든 팔로잉 해제" -removeAllFollowingDescription: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않게 된 경우 등에 실행해 주세요." +removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않는 경우 등에 실행해 주세요." userSuspended: "이 계정은 정지된 상태입니다." userSilenced: "이 계정은 사일런스된 상태입니다." yourAccountSuspendedTitle: "계정이 정지되었습니다" @@ -587,7 +599,7 @@ addedRelays: "추가된 릴레이" serviceworkerInfo: "푸시 알림을 수행하려면 활성화해야 합니다." deletedNote: "삭제된 노트" invisibleNote: "비공개 노트" -enableInfiniteScroll: "자동으로 좀 더 보기" +enableInfiniteScroll: "자동으로 더 보기" visibility: "공개 범위" poll: "투표" useCw: "내용 숨기기" @@ -628,13 +640,14 @@ emailAddress: "메일 주소" smtpConfig: "SMTP 서버 설정" smtpHost: "호스트" smtpPort: "포트" -smtpUser: "유저명" +smtpUser: "사용자 이름" smtpPass: "비밀번호" emptyToDisableSmtpAuth: "SMTP 인증을 사용하지 않으려면 공란으로 비워둡니다." smtpSecure: "SMTP 연결에 Implicit SSL/TTS 사용" smtpSecureInfo: "STARTTLS 사용 시에는 해제합니다." testEmail: "이메일 전송 테스트" wordMute: "단어 뮤트" +hardWordMute: "하드 단어 뮤트" regexpError: "정규 표현식 오류" regexpErrorDescription: "{tab}단어 뮤트 {line}행의 정규 표현식에 오류가 발생했습니다:" instanceMute: "서버 뮤트" @@ -662,7 +675,7 @@ behavior: "동작" sample: "예시" abuseReports: "신고" reportAbuse: "신고" -reportAbuseRenote: "Renote를 신고" +reportAbuseRenote: "리노트 신고하기" reportAbuseOf: "{name}을 신고하기" fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요." abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다." @@ -679,7 +692,7 @@ defaultNavigationBehaviour: "기본 탐색 동작" editTheseSettingsMayBreakAccount: "이 설정을 변경하면 계정이 손상될 수 있습니다." instanceTicker: "노트의 서버 정보" waitingFor: "{x}을(를) 기다리고 있습니다" -random: "랜덤" +random: "무작위" system: "시스템" switchUi: "UI 전환" desktop: "데스크탑" @@ -696,9 +709,9 @@ manageAccessTokens: "액세스 토큰 관리" accountInfo: "계정 정보" notesCount: "노트 수" repliesCount: "답글 수" -renotesCount: "Renote 수" +renotesCount: "리노트 수" repliedCount: "받은 답글 수" -renotedCount: "받은 Renote 수" +renotedCount: "받은 리노트 수" followingCount: "팔로우 수" followersCount: "팔로워 수" sentReactionsCount: "보낸 리액션 수" @@ -804,7 +817,7 @@ switchAccount: "계정 바꾸기" enabled: "활성화" disabled: "비활성화" quickAction: "빠른 동작" -user: "유저" +user: "사용자" administration: "관리" accounts: "계정" switch: "전환" @@ -831,7 +844,7 @@ previewNoteText: "본문 미리보기" customCss: "CSS 사용자화" customCssWarn: "이 설정은 기능을 알고 있는 경우에만 사용해야 합니다. 잘못된 값을 입력하면 클라이언트가 정상적으로 작동하지 않을 수 있습니다." global: "글로벌" -squareAvatars: "프로필 아이콘을 사각형으로 표시" +squareAvatars: "프로필 아바타를 사각형으로 표시" sent: "전송" received: "수신" searchResult: "검색 결과" @@ -850,8 +863,8 @@ devMode: "개발자 모드" keepCw: "CW 유지하기" pubSub: "Pub/Sub 계정" lastCommunication: "마지막 통신" -resolved: "해결됨" -unresolved: "해결되지 않음" +resolved: "처리함" +unresolved: "처리되지 않음" breakFollow: "팔로워 해제" breakFollowConfirm: "팔로우를 해제하시겠습니까?" itsOn: "켜져 있습니다" @@ -866,11 +879,11 @@ manageAccounts: "계정 관리" makeReactionsPublic: "리액션 목록을 공개하기" makeReactionsPublicDescription: "나의 리액션을 누구나 볼 수 있게 합니다." classic: "클래식" -muteThread: "이 글타래를 뮤트" +muteThread: "글타래 뮤트" unmuteThread: "글타래 뮤트 해제" -ffVisibility: "내 인맥의 공개 범위" -ffVisibilityDescription: "나의 팔로우와 팔로워 정보에 대한 공개 범위를 설정할 수 있습니다." -continueThread: "이 글타래 이어서 보기" +followingVisibility: "팔로우의 공개 범위" +followersVisibility: "팔로워의 공개 범위" +continueThread: "글타래 더 보기" deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? " incorrectPassword: "비밀번호가 올바르지 않습니다." voteConfirm: "\"{choice}\"에 투표하시겠습니까?" @@ -969,7 +982,7 @@ show: "표시" neverShow: "다시 보지 않기" remindMeLater: "나중에 알림" didYouLikeMisskey: "Misskey가 마음에 드시나요?" -pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!" +pleaseDonate: "Misskey는 {host} 서버의 무료 소프트웨어입니다. 앞으로도 개발을 이어 나가려면 후원이 절실히 필요합니다!" roles: "역할" role: "역할" noRole: "역할이 없습니다" @@ -1014,12 +1027,14 @@ reactionAcceptance: "리액션 수신" likeOnly: "좋아요만 받기" likeOnlyForRemote: "리모트에서는 좋아요만 받기" nonSensitiveOnly: "민감한 이모지를 제외하고 받기" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "민감한 이모지를 제외하고 받기 (리모트에서는 좋아요만 받기)" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "민감한 이모지를 제외하고 받기(리모트에서는 좋아요만 받기)" rolesAssignedToMe: "나에게 할당된 역할" resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?" sensitiveWords: "민감한 단어" sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '홈'으로 강제합니다. 개행으로 구분하여 여러 개를 지정할 수 있습니다." sensitiveWordsDescription2: "공백으로 구분하면 AND 지정이 되며, 키워드를 슬래시로 둘러싸면 정규 표현식이 됩니다." +hiddenTags: "숨긴 해시태그" +hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다." notesSearchNotAvailable: "노트 검색을 이용하실 수 없습니다." license: "라이선스" unfavoriteConfirm: "즐겨찾기를 해제하시겠습니까?" @@ -1032,6 +1047,7 @@ enableChartsForRemoteUser: "리모트 유저의 차트를 생성" enableChartsForFederatedInstances: "리모트 서버의 차트를 생성" showClipButtonInNoteFooter: "노트 동작에 클립을 추가" reactionsDisplaySize: "리액션 표시 크기" +limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시하기" noteIdOrUrl: "노트 ID 및 URL" video: "동영상" videos: "동영상" @@ -1044,7 +1060,7 @@ forceShowAds: "광고를 항상 표시" addMemo: "메모 추가" editMemo: "메모 편집" reactionsList: "리액션 목록" -renotesList: "Renote 목록" +renotesList: "리노트 목록" notificationDisplay: "알림 표시" leftTop: "왼쪽 상단" rightTop: "오른쪽 상단" @@ -1109,7 +1125,7 @@ beSureToReadThisAsItIsImportant: "중요하므로 반드시 읽어주십시오." iHaveReadXCarefullyAndAgree: "\"{x}\"의 내용을 읽고 동의합니다." dialog: "다이얼로그" icon: "아바타" -forYou: "당신에게" +forYou: "나에게" currentAnnouncements: "현재 공지사항" pastAnnouncements: "과거 공지사항" youHaveUnreadAnnouncements: "읽지 않은 공지사항이 있습니다." @@ -1144,12 +1160,13 @@ impressumDescription: "독일 등의 일부 나라와 지역에서는 꼭 표시 privacyPolicy: "개인정보 보호 정책" privacyPolicyUrl: "개인정보 보호 정책 URL" tosAndPrivacyPolicy: "약관 및 개인정보 보호 정책" -avatarDecorations: "아이콘 장식" +avatarDecorations: "아바타 장식" attach: "붙이기" -detach: "떼기" +detach: "빼기" +detachAll: "모두 빼기" angle: "각도" flip: "플립" -showAvatarDecorations: "아이콘 장식을 표시" +showAvatarDecorations: "아바타 장식 표시" releaseToRefresh: "놓아서 새로고침" refreshing: "새로고침 중" pullDownToRefresh: "아래로 내려서 새로고침" @@ -1158,6 +1175,14 @@ useGroupedNotifications: "알림을 그룹화하고 표시" signupPendingError: "메일 주소 확인중에 문제가 발생했습니다. 링크의 유효기간이 지났을 가능성이 있습니다." cwNotationRequired: "'내용을 숨기기'를 체크한 경우 주석을 써야 합니다." doReaction: "리액션 추가" +code: "문자열" +reloadRequiredToApplySettings: "설정을 적용하려면 새로고침을 해야 합니다." +remainingN: "나머지: {n}" +overwriteContentConfirm: "현재 내용을 덮어쓰기 합니다. 계속 진행하시겠습니까?" +seasonalScreenEffect: "계절에 따른 효과 보이기" +decorate: "장식하기" +addMfmFunction: "장식 추가하기" +enableQuickAddMfmFunction: "상급자용 MFM 선택기 표시하기" _announcement: forExistingUsers: "기존 유저에게만 알림" forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다." @@ -1198,7 +1223,7 @@ _initialTutorial: _note: title: "'노트'가 무엇인가요?" description: "미스키에서는 게시물을 '노트'라고 합니다. 노트는 타임라인에 시간순으로 정렬되어 있고, 실시간으로 갱신됩니다." - reply: "답글을 다는 것이 가능합니다. 답글에 답글을 다는 것도 가능하며 스레드처럼 대화를 계속하는 것도 가능합니다." + reply: "답글을 달 수 있습니다. 답글에 답글을 달 수도 있고 글타래처럼 대화를 이어갈 수도 있습니다." renote: "그 노트를 자기 타임라인에 가져와서 공유하는 것이 가능합니다. 글을 추가해서 인용하는 것도 가능합니다." reaction: "리액션을 다는 것이 가능합니다. 다음 페이지에서 자세한 설명을 볼 수 있습니다." menu: "노트의 상세 정보를 표시하거나, 링크를 복사하는 등의 다양한 조작을 할 수 있습니다." @@ -1211,7 +1236,7 @@ _initialTutorial: reactDone: "'-' 버튼을 눌러서 리액션을 취소할 수 있습니다." _timeline: title: "타임라인에 대하여" - description1: "Misskey에는 종류에 따라 여러 가지의 타임라인으로 구성되어 있습니다. (서버에 따라서는 일부 타임라인을 사용할 수 없는 경우가 있습니다)" + description1: "Misskey에는 종류에 따라 여러 가지의 타임라인으로 구성되어 있습니다.(서버에 따라서는 일부 타임라인을 사용할 수 없는 경우가 있습니다)" home: "내가 팔로우 중인 계정의 노트를 볼 수 있습니다." local: "이 서버에 있는 모든 유저의 게시물을 볼 수 있습니다." social: "홈 타임라인과 로컬 타임라인의 게시물을 모두 볼 수 있습니다." @@ -1266,6 +1291,8 @@ _serverSettings: shortName: "약칭" shortNameDescription: "서버의 정식 명칭이 긴 경우에, 대신에 표시할 수 있는 약칭이나 통칭." fanoutTimelineDescription: "활성화하면 각종 타임라인을 가져올 때의 성능을 대폭 향상하며, 데이터베이스의 부하를 줄일 수 있습니다. 단, Redis의 메모리 사용량이 증가합니다. 서버의 메모리 용량이 작거나, 서비스가 불안정해지는 경우 비활성화할 수 있습니다." + fanoutTimelineDbFallback: "데이터베이스를 예비로 사용하기" + fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해 DB에 질의하여 정보를 가져옵니다. 비활성화하면 이를 실행하지 않음으로써 서버의 부하를 줄일 수 있지만, 타임라인에서 가져올 수 있는 게시물 범위가 한정됩니다." _accountMigration: moveFrom: "다른 계정에서 이 계정으로 이사" moveFromSub: "다른 계정에 대한 별칭을 생성" @@ -1285,29 +1312,29 @@ _achievements: earnedAt: "달성 일시" _types: _notes1: - title: "미스키 시작했는데요" + title: "미스키 계정 만들었어요" description: "첫 노트를 작성했습니다" - flavor: "Misskey에 오신 것을 환영합니다!" + flavor: "Misskey에 어서 오세요!" _notes10: - title: "노트 조금" + title: "몇 가지 노트" description: "10개의 노트를 작성했습니다" _notes100: - title: "노트 많이" + title: "많은 노트" description: "100개의 노트를 작성했습니다" _notes500: - title: "노트로 뒤덮여버렸어" + title: "노트 범벅" description: "500개의 노트를 작성했습니다" _notes1000: - title: "노트만 산더미" + title: "노트가 산더미" description: "1,000개의 노트를 작성했습니다" _notes5000: - title: "노트가 어디서 솟아?" + title: "솟아나는 노트" description: "5,000개의 노트를 작성했습니다" _notes10000: title: "슈퍼 노트" description: "10,000개의 노트를 작성했습니다" _notes20000: - title: "노트 더 없어?" + title: "노트가 필요해요" description: "20,000개의 노트를 작성했습니다" _notes30000: title: "노트노트노트" @@ -1333,27 +1360,27 @@ _achievements: _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" description: "100,000개의 노트를 작성했습니다" - flavor: "이만큼 쓸 일도 없겠지만... 다른 할 일이 있진 않으신가요?" + flavor: "이렇게나 쓸 게 있어요?" _login3: - title: "비기너 I" - description: "총 3일간 로그인했습니다" - flavor: "오늘부터 여러분도 미스키스트에요!" + title: "초보자 I" + description: "총 로그인한 날이 3일" + flavor: "오늘부터 여러분도 미스키스트랍니다" _login7: - title: "비기너 II" - description: "총 7일간 로그인했습니다" + title: "초보자 II" + description: "총 로그인한 날이 7일" flavor: "슬슬 익숙해지셨나요?" _login15: - title: "비기너 III" - description: "총 15일간 로그인했습니다" + title: "초보자 III" + description: "총 로그인한 날이 15일" _login30: title: "미스키스트 I" - description: "총 30일간 로그인했습니다" + description: "총 로그인한 날이 30일" _login60: title: "미스키스트 II" - description: "총 60일간 로그인했습니다" + description: "총 로그인한 날이 60일" _login100: title: "미스키스트 III" - description: "총 100일간 로그인했습니다" + description: "총 로그인한 날이 100일" flavor: "그 유저, 미스키스트이다" _login200: title: "단골 I" @@ -1450,7 +1477,7 @@ _achievements: title: "보물찾기" description: "숨겨진 보물을 발견했습니다" _client30min: - title: "잠깐 쉬어" + title: "잠시 쉬어요" description: "클라이언트를 시작하고 30분이 경과하였습니다" _client60min: title: "No \"Miss\" in Misskey" @@ -1488,8 +1515,8 @@ _achievements: title: "읽고 답하긴 하시는 건가요?" description: "100자가 넘는 노트가 작성되고 3초 안에 반응했습니다" _clickedClickHere: - title: "여길 눌러보세요" - description: "여길을 눌러봤습니다" + title: "여기를 누르세요" + description: "여기를 눌렀습니다" _justPlainLucky: title: "그냥 운이 좋았어" description: "매 10초마다 0.01%의 확률로 달성됩니다" @@ -1532,11 +1559,13 @@ _role: name: "역할 이름" description: "역할 설명" permission: "역할 권한" - descriptionOfPermission: "모더레이터는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n관리자는 서버의 모든 설정을 변경할 수 있습니다." + descriptionOfPermission: "조정자는 기본적인 조정 작업을 진행할 수 있습니다.\n관리자는 서버의 모든 설정을 변경할 수 있습니다." assignTarget: "할당 대상" descriptionOfAssignTarget: "수동을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n조건부를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다." manual: "수동" + manualRoles: "수동 역할" conditional: "조건부" + conditionalRoles: "조건부 역할" condition: "조건" isConditionalRole: "조건부 역할입니다." isPublic: "역할 공개" @@ -1585,6 +1614,7 @@ _role: canHideAds: "광고 숨기기" canSearchNotes: "노트 검색 이용 가능 여부" canUseTranslator: "번역 기능의 사용" + avatarDecorationLimit: "아바타 장식의 최대 붙임 개수" _condition: isLocal: "로컬 사용자" isRemote: "리모트 사용자" @@ -1600,7 +1630,7 @@ _role: or: "다음을 하나라도 만족" not: "다음을 만족하지 않음" _sensitiveMediaDetection: - description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다." + description: "기계 학습으로 민감한 미디어를 알아서 찾아내어 조정에 참고하도록 합니다. 서버가 부하를 다소 받습니다." sensitivity: "탐지 민감도" sensitivityDescription: "민감도가 낮을수록 안전한 미디어가 잘못 탐지될 확률이 줄어들며, 높을수록 민감한 미디어가 탐지되지 않을 확률이 줄어듭니다." setSensitiveFlagAutomatically: "자동으로 NSFW로 설정하기" @@ -1613,6 +1643,7 @@ _emailUnavailable: disposable: "임시 이메일 주소는 사용할 수 없습니다" mx: "메일 서버가 올바르지 않습니다" smtp: "메일 서버가 응답하지 않습니다" + banned: "이 메일 주소는 사용할 수 없습니다" _ffVisibility: public: "공개" followers: "팔로워에게만 공개" @@ -1681,7 +1712,7 @@ _registry: domain: "도메인" createKey: "키 생성" _aboutMisskey: - about: "Misskey는 syuilo에 의해서 2014년부터 개발되어 온 오픈소스 소프트웨어 입니다." + about: "Misskey는 syuilo가 2014년부터 개발한 오픈소스 소프트웨어입니다." contributors: "주요 기여자" allContributors: "모든 기여자" source: "소스 코드" @@ -1796,7 +1827,7 @@ _theme: driveFolderBg: "드라이브 폴더 배경" wallpaperOverlay: "배경화면 오버레이" badge: "배지" - messageBg: "채팅 배경" + messageBg: "대화 배경" accentDarken: "강조 색상 (어두움)" accentLighten: "강조 색상 (밝음)" fgHighlighted: "강조된 텍스트" @@ -1806,6 +1837,14 @@ _sfx: notification: "알림" antenna: "안테나 수신" channel: "채널 알림" + reaction: "리액션 선택" +_soundSettings: + driveFile: "드라이브에 있는 오디오를 사용" + driveFileWarn: "드라이브에 있는 파일을 선택하세요." + driveFileTypeWarn: "이 파일은 지원되지 않습니다." + driveFileTypeWarnDescription: "오디오 파일을 선택하세요." + driveFileDurationWarn: "오디오가 너무 깁니다" + driveFileDurationWarnDescription: "긴 오디오로 설정할 경우 미스키 사용에 지장이 갈 수도 있습니다. 그래도 괜찮습니까?" _ago: future: "미래" justNow: "방금 전" @@ -1817,6 +1856,14 @@ _ago: monthsAgo: "{n}개월 전" yearsAgo: "{n}년 전" invalid: "없음" +_timeIn: + seconds: "{n}초 후" + minutes: "{n}분 후" + hours: "{n}시간 후" + days: "{n}일 후" + weeks: "{n}주 후" + months: "{n}개월 후" + years: "{n}년 후" _time: second: "초" minute: "분" @@ -1856,9 +1903,9 @@ _permissions: "write:account": "계정의 정보를 변경합니다" "read:blocks": "차단 여부를 확인합니다" "write:blocks": "차단을 하거나 해제합니다" - "read:drive": "드라이브를 조회합니다" + "read:drive": "드라이브 보기" "write:drive": "드라이브에 파일을 올리거나, 이름을 변경하거나, 삭제합니다" - "read:favorites": "즐겨찾기를 조회합니다" + "read:favorites": "즐겨찾기 보기" "write:favorites": "즐겨찾기에 추가하거나 삭제합니다" "read:following": "팔로우 상태를 봅니다" "write:following": "팔로우하거나 팔로우를 해제합니다" @@ -1876,7 +1923,7 @@ _permissions: "write:pages": "페이지를 수정합니다" "read:page-likes": "페이지의 좋아요를 확인합니다" "write:page-likes": "페이지에 좋아요를 추가하거나 취소합니다" - "read:user-groups": "유저 그룹을 조회합니다" + "read:user-groups": "사용자 그룹 보기" "write:user-groups": "유저 그룹을 만들거나, 초대하거나, 이름을 변경하거나, 양도하거나, 삭제합니다" "read:channels": "채널을 보기" "write:channels": "채널을 추가하거나 삭제합니다" @@ -1888,6 +1935,55 @@ _permissions: "write:flash": "Play를 조작합니다" "read:flash-likes": "Play의 좋아요를 봅니다" "write:flash-likes": "Play의 좋아요를 조작합니다" + "read:admin:abuse-user-reports": "사용자 신고 보기" + "write:admin:delete-account": "사용자 계정 삭제하기" + "write:admin:delete-all-files-of-a-user": "모든 사용자 파일 삭제하기" + "read:admin:index-stats": "데이터베이스 색인 정보 보기" + "read:admin:table-stats": "데이터베이스 테이블 정보 보기" + "read:admin:user-ips": "사용자 IP 주소 보기" + "read:admin:meta": "인스턴스 메타데이터 보기" + "write:admin:reset-password": "사용자 비밀번호 재설정하기" + "write:admin:resolve-abuse-user-report": "사용자 신고 처리하기" + "write:admin:send-email": "이메일 보내기" + "read:admin:server-info": "서버 정보 보기" + "read:admin:show-moderation-log": "조정 기록 보기" + "read:admin:show-user": "사용자 개인정보 보기" + "read:admin:show-users": "사용자 개인정보 보기" + "write:admin:suspend-user": "사용자 정지하기" + "write:admin:unset-user-avatar": "사용자 아바타 삭제하기" + "write:admin:unset-user-banner": "사용자 배너 삭제하기" + "write:admin:unsuspend-user": "사용자 정지 해제하기" + "write:admin:meta": "인스턴스 메타데이터 수정하기" + "write:admin:user-note": "조정 기록 수정하기" + "write:admin:roles": "역할 수정하기" + "read:admin:roles": "역할 보기" + "write:admin:relays": "릴레이 수정하기" + "read:admin:relays": "릴레이 보기" + "write:admin:invite-codes": "초대 코드 수정하기" + "read:admin:invite-codes": "초대 코드 보기" + "write:admin:announcements": "공지사항 수정하기" + "read:admin:announcements": "공지사항 보기" + "write:admin:avatar-decorations": "아바타 꾸미기 수정하기" + "read:admin:avatar-decorations": "아바타 꾸미기 보기" + "write:admin:federation": "연합 정보 수정하기" + "write:admin:account": "사용자 계정 수정하기" + "read:admin:account": "사용자 정보 보기" + "write:admin:emoji": "이모지 수정하기" + "read:admin:emoji": "이모지 보기" + "write:admin:queue": "작업 대기열 수정하기" + "read:admin:queue": "작업 대기열 정보 보기" + "write:admin:promo": "홍보 기록 수정하기" + "write:admin:drive": "사용자 드라이브 수정하기" + "read:admin:drive": "사용자 드라이브 정보 보기" + "read:admin:stream": "관리자용 Websocket API 사용하기" + "write:admin:ad": "광고 수정하기" + "read:admin:ad": "광고 보기" + "write:invite-codes": "초대 코드 만들기" + "read:invite-codes": "초대 코드 불러오기" + "write:clip-favorite": "클립의 좋아요 수정하기" + "read:clip-favorite": "클립의 좋아요 보기" + "read:federation": "연합 정보 불러오기" + "write:report-abuse": "위반 내용 신고하기" _auth: shareAccessTitle: "어플리케이션의 접근 허가" shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?" @@ -1942,6 +2038,7 @@ _widgets: _userList: chooseList: "리스트 선택" clicker: "클리커" + birthdayFollowings: "오늘이 생일인 사용자" _cw: hide: "숨기기" show: "더 보기" @@ -1989,7 +2086,7 @@ _postForm: b: "무슨 일이 일어나고 있나요?" c: "무엇을 생각하고 있나요?" d: "말하고 싶은 게 있나요?" - e: "여기에 적어주세요" + e: "여기에 적어 주세요" f: "작성해주시길 기다리고 있어요..." _profile: name: "이름" @@ -2004,6 +2101,7 @@ _profile: changeAvatar: "아바타 이미지 변경" changeBanner: "배너 이미지 변경" verifiedLinkDescription: "내용에 자신의 프로필로 향하는 링크가 포함된 페이지의 URL을 삽입하면 소유자 인증 마크가 표시됩니다." + avatarDecorationMax: "최대 {max}개까지 장식을 할 수 있습니다." _exportOrImport: allNotes: "모든 노트" favoritedNotes: "즐겨찾기한 노트" @@ -2118,13 +2216,14 @@ _notification: youGotMention: "{name}님이 멘션함" youGotReply: "{name}님이 답글함" youGotQuote: "{name}님이 인용함" - youRenoted: "{name}님이 Renote" + youRenoted: "{name}님이 리노트했습니다" youWereFollowed: "새로운 팔로워가 있습니다" youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다" yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다" pollEnded: "투표 결과가 발표되었습니다" newNote: "새 게시물" unreadAntennaNote: "안테나 {name}" + roleAssigned: "역할이 부여 되었습니다." emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다" achievementEarned: "도전 과제를 달성했습니다" testNotification: "알림 테스트" @@ -2146,6 +2245,7 @@ _notification: pollEnded: "투표가 종료됨" receiveFollowRequest: "팔로우 요청을 받았을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때" + roleAssigned: "역할이 부여 됨" achievementEarned: "도전 과제 획득" app: "연동된 앱을 통한 알림" _actions: @@ -2203,7 +2303,7 @@ _webhookSettings: followed: "누군가 나를 팔로우했을 때" note: "노트를 게시할 때" reply: "답글을 받았을 때" - renote: "누군가 내 글을 Renote했을 때" + renote: "누군가 내 글을 리노트했을 때" reaction: "누군가 내 노트에 리액션했을 때" mention: "누군가 나를 멘션했을 때" _moderationLogTypes: @@ -2218,28 +2318,30 @@ _moderationLogTypes: updateCustomEmoji: "커스텀 이모지 수정" deleteCustomEmoji: "커스텀 이모지 삭제" updateServerSettings: "서버 설정 갱신" - updateUserNote: "모더레이션 노트 갱신" + updateUserNote: "조정 기록 갱신" deleteDriveFile: "파일 삭제" deleteNote: "노트 삭제" - createGlobalAnnouncement: "전역 공지사항 생성" - createUserAnnouncement: "유저 공지사항 생성" - updateGlobalAnnouncement: "전역 공지사항 수정" - updateUserAnnouncement: "유저 공지사항 수정" - deleteGlobalAnnouncement: "전역 공지사항 삭제" - deleteUserAnnouncement: "유저 공지사항 삭제" + createGlobalAnnouncement: "모든 공지사항 만들기" + createUserAnnouncement: "사용자 공지사항 만들기" + updateGlobalAnnouncement: "모든 공지사항 수정" + updateUserAnnouncement: "사용자 공지사항 수정" + deleteGlobalAnnouncement: "모든 공지사항 삭제" + deleteUserAnnouncement: "사용자 공지사항 삭제" resetPassword: "비밀번호 재설정" suspendRemoteInstance: "리모트 서버를 정지" unsuspendRemoteInstance: "리모트 서버의 정지를 해제" markSensitiveDriveFile: "파일에 열람주의를 설정" unmarkSensitiveDriveFile: "파일에 열람주의를 해제" - resolveAbuseReport: "신고 해결" + resolveAbuseReport: "신고 처리" createInvitation: "초대 코드 생성" createAd: "광고 생성" deleteAd: "광고 삭제" updateAd: "광고 수정" - createAvatarDecoration: "아이콘 장식 추가" - updateAvatarDecoration: "아이콘 장식 수정" - deleteAvatarDecoration: "아이콘 장식 삭제" + createAvatarDecoration: "아바타 장식 만들기" + updateAvatarDecoration: "아바타 장식 수정" + deleteAvatarDecoration: "아바타 장식 삭제" + unsetUserAvatar: "유저 아바타 제거" + unsetUserBanner: "유저 배너 제거" _fileViewer: title: "파일 상세" type: "파일 유형" @@ -2289,3 +2391,16 @@ _externalResourceInstaller: _themeInstallFailed: title: "테마를 설치하지 못했습니다" description: "테마를 설치하는 도중 문제가 발생하였습니다. 다시 한 번 시도하십시오. 자세한 사항은 브라우저에 내장된 개발자 도구의 Javascript 콘솔에서 확인하실 수 있습니다." +_dataSaver: + _media: + title: "미디어 불러오기" + description: "사진이나 동영상을 자동으로 불러오지 않습니다. 숨겨 놓은 사진이나 동영상은 누르면 불러옵니다." + _avatar: + title: "아이콘 이미지" + description: "아이콘 이미지의 애니메이션을 멈춥니다. 애니메이션 이미지는 일반 이미지보다 파일 크기가 클 수 있으므로 데이터 사용량을 더 줄일 수 있습니다." + _urlPreview: + title: "URL 미리보기의 섬네일" + description: "URL 미리보기의 섬네일 이미지를 불러오지 않게 됩니다." + _code: + title: "문자열 강조" + description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다." diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index b22e047cfa..c9e5aea1ed 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -320,7 +320,6 @@ administrator: "ຜູ້ບໍລິຫານ" token: "ໂທເຄັນ" share: "ແບ່ງປັນ" notFound: "ບໍ່ພົບ" -cacheClear: "ລຶບລ້າງແຄສ" help: "ຊ່ວຍເຫຼືອ" close: "ປິດ" invites: "ເຊີນ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 42a29a5ad3..1e96a1aa98 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -119,7 +119,6 @@ sensitive: "NSFW" add: "Toevoegen" reaction: "Reacties" reactions: "Reacties" -reactionSetting: "Reacties die in de reactie-selector worden getoond" reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen" rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen" attachCancel: "Verwijder bijlage" @@ -396,7 +395,6 @@ reduceUiAnimation: "Verminder beweging in de UI" share: "Delen" notFound: "Niet gevonden" uploadFolder: "Standaardmap voor uploaden" -cacheClear: "Cache verwijderen" markAsReadAllNotifications: "Markeer alle meldingen als gelezen" markAsReadAllUnreadNotes: "Markeer alle berichten als gelezen" markAsReadAllTalkMessages: "Markeer alle berichten als gelezen" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 44944f8465..195b1d0717 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -102,7 +102,6 @@ clickToShow: "Klikk for å vise" add: "Legg til" reaction: "Reaksjon" reactions: "Reaksjoner" -reactionSetting: "Reaksjoner som vises i reaksjonsvelgeren" reactionSettingDescription2: "Dra for å endre rekkefølgen, klikk for å slette, trykk \"+\" for å legge til." rememberNoteVisibility: "Husk innstillingene for synlighet av Notes" attachCancel: "Fjern vedlegg" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index c1f91dc501..b0f9f4923d 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -111,7 +111,6 @@ sensitive: "NSFW" add: "Dodaj" reaction: "Reakcja" reactions: "Reakcja" -reactionSetting: "Reakcje do pokazania w wyborniku reakcji" reactionSettingDescription2: "Przeciągnij aby zmienić kolejność, naciśnij aby usunąć, naciśnij „+” aby dodać" rememberNoteVisibility: "Zapamiętuj ustawienia widoczności wpisu" attachCancel: "Usuń załącznik" @@ -407,7 +406,6 @@ share: "Udostępnij" notFound: "Nie znaleziono" notFoundDescription: "Nie ma strony odpowiadającej określonemu adresowi URL." uploadFolder: "Domyślne położenie wysłanych" -cacheClear: "Wyczyść pamięć podręczną" markAsReadAllNotifications: "Oznacz wszystkie powiadomienia jako przeczytane" markAsReadAllUnreadNotes: "Oznacz wszystkie wpisy jako przeczytane" markAsReadAllTalkMessages: "Oznacz wszystkie wiadomości jako przeczytane" @@ -808,8 +806,6 @@ makeReactionsPublicDescription: "To spowoduje, że lista wszystkich Twoich dotyc classic: "Klasyczny" muteThread: "Wycisz wątek" unmuteThread: "Wyłącz wyciszenie wątku" -ffVisibility: "Widoczność obserwowanych/obserwujących" -ffVisibilityDescription: "Pozwala skonfigurować, kto może zobaczyć, kogo obserwujesz i kto Cię obserwuje." continueThread: "Pokaż kontynuację wątku" deleteAccountConfirm: "Spowoduje to nieodwracalne usunięcie Twojego konta. Kontynuować?" incorrectPassword: "Nieprawidłowe hasło." diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 52db84911d..1fd2fd57e1 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -121,7 +121,6 @@ sensitive: "Conteúdo sensível" add: "Adicionar" reaction: "Reações" reactions: "Reações" -reactionSetting: "Quais reações exibir no seletor de reações" reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar." rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" attachCancel: "Remover anexo" @@ -431,7 +430,6 @@ share: "Compartilhar" notFound: "Não encontrado" notFoundDescription: "Não havia página correspondente ao URL especificado." uploadFolder: "Destino de upload padrão" -cacheClear: "Excluir memória transitória" markAsReadAllNotifications: "Marcar todas as notificações como lidas" markAsReadAllUnreadNotes: "Marcar todas as postagens como lidas" markAsReadAllTalkMessages: "Marcar todas as conversas como lidas" @@ -860,8 +858,6 @@ makeReactionsPublicDescription: "Isto vai deixar o histórico de todas as suas r classic: "Clássico" muteThread: "Silenciar esta conversa" unmuteThread: "Desativar silêncio desta conversa" -ffVisibility: "Visibilidade de Seguidos/Seguidores" -ffVisibilityDescription: "Permite configurar quem pode ver quem lhe segue e quem você está seguindo." continueThread: "Ver mais desta conversa" deleteAccountConfirm: "Deseja realmente excluir a conta?" incorrectPassword: "Senha inválida." diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 6e43e38630..bf8787413b 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -2,6 +2,7 @@ _lang_: "Română" headlineMisskey: "O rețea conectată prin note" introMisskey: "Bine ai venit! Misskey este un serviciu de microblogging open source și decentralizat.\nCreează \"note\" cu care să îți poți împărți gândurile cu oricine din jurul tău. 📡\nCu \"reacții\" îți poți expirma rapid părerea despre notele oricui. 👍\nHai să explorăm o lume nouă! 🚀" +poweredByMisskeyDescription: "{name} este unul dintre serviciile care se folosește de platforma open source Misskey." monthAndDay: "{day}/{month}" search: "Caută" notifications: "Notificări" @@ -12,12 +13,14 @@ fetchingAsApObject: "Se aduce din Fediverse..." ok: "OK" gotIt: "Am înțeles!" cancel: "Anulează" +noThankYou: "Nu, mulțumesc." enterUsername: "Introdu numele de utilizator" renotedBy: "Re-notat de {user}" noNotes: "Nicio notă" noNotifications: "Nicio notificare" instance: "Instanță" settings: "Setări" +notificationSettings: "Setări notificări" basicSettings: "Setări generale" otherSettings: "Alte Setări" openInWindow: "Deschide într-o fereastră" @@ -42,12 +45,20 @@ pin: "Fixează pe profil" unpin: "Anulati fixare" copyContent: "Copiază conținutul" copyLink: "Copiază link-ul" +copyLinkRenote: "Copiază linkul pentru renote" delete: "Şterge" deleteAndEdit: "Șterge și editează" deleteAndEditConfirm: "Ești sigur că vrei să ștergi această notă și să o editezi? Vei pierde reacțiile, re-notele și răspunsurile acesteia." addToList: "Adaugă în listă" +addToAntenna: "Adaugă la antenă" sendMessage: "Trimite un mesaj" +copyRSS: "Copiază RSS" copyUsername: "Copiază numele de utilizator" +copyUserId: "Copiază numele de utilizator" +copyNoteId: "Copiază ID-ul notiței" +copyFileId: "Copiază ID-ul fișierului" +copyFolderId: "Copiază ID-ul folderului" +copyProfileUrl: "Copiază URL profil" searchUser: "Caută un utilizator" reply: "Răspunde" loadMore: "Incarcă mai mult" @@ -100,6 +111,8 @@ renoted: "Re-notat." cantRenote: "Această postare nu poate fi re-notată." cantReRenote: "O re-notă nu poate fi re-notată." quote: "Citează" +inChannelRenote: "Renotează în canal" +inChannelQuote: "Citează în canal" pinnedNote: "Notă fixată" pinned: "Fixat pe profil" you: "Tu" @@ -108,7 +121,6 @@ sensitive: "NSFW" add: "Adaugă" reaction: "Reacție" reactions: "Reacție" -reactionSetting: "Reacții care să apară in selectorul de reacții" reactionSettingDescription2: "Trage pentru a rearanja, apasă pe \"+\" pentru a adăuga." rememberNoteVisibility: "Amintește setarea de vizibilitate a notelor" attachCancel: "Înlătură atașament" @@ -117,6 +129,8 @@ unmarkAsSensitive: "Demarchează ca NSFW" enterFileName: "Introduceţi numele fişierului" mute: "Amuțește" unmute: "Înlătură amuțirea" +renoteMute: "Renotări pe modul silențios" +renoteUnmute: "Scoate renotările de pe modul silențios" block: "Blochează" unblock: "Deblochează" suspend: "Suspendă" @@ -126,7 +140,10 @@ unblockConfirm: "Ești sigur ca vrei să deblochezi acest cont?" suspendConfirm: "Ești sigur ca vrei să suspendezi acest cont?" unsuspendConfirm: "Ești sigur ca vrei să nu mai suspendezi acest cont?" selectList: "Selectează o listă" +editList: "Editați lista" +selectChannel: "Selectaţi canalul" selectAntenna: "Selectează o antenă" +editAntenna: "Editează antena" selectWidget: "Selectați un widget" editWidgets: "Editează widget-urile" editWidgetsExit: "Terminat" @@ -139,6 +156,7 @@ addEmoji: "Adaugă un emoji" settingGuide: "Setări recomandate" cacheRemoteFiles: "Ține fișierele externe in cache" cacheRemoteFilesDescription: "Când această setare este dezactivată, fișierele externe sunt încărcate direct din instanța externă. Dezactivarea va scădea utilizarea spațiului de stocare, dar va crește traficul, deoarece thumbnail-urile nu vor fi generate." +youCanCleanRemoteFilesCache: "Poți goli cache-ul prin a apăsa pe butonul de 🗑️ din fereastra de gestionare a fișierelor." flagAsBot: "Marchează acest cont ca bot" flagAsBotDescription: "Activează această opțiune dacă acest cont este controlat de un program. Daca e activată, aceasta va juca rolul unui indicator pentru dezvoltatori pentru a preveni interacțiunea în lanțuri infinite cu ceilalți boți și ajustează sistemele interne al Misskey pentru a trata acest cont drept un bot." flagAsCat: "Marchează acest cont ca pisică" @@ -393,7 +411,6 @@ share: "Distribuie" notFound: "Nu a fost găsit" notFoundDescription: "N-a fost găsită nicio pagină cu acest URL." uploadFolder: "Folder implicit pentru încărcări" -cacheClear: "Golește cache-ul" markAsReadAllNotifications: "Marchează toate notificările drept citit" markAsReadAllUnreadNotes: "Marchează toate notele drept citit" markAsReadAllTalkMessages: "Marchează toate mesajele drept citit" @@ -649,6 +666,8 @@ _sfx: notification: "Notificări" _ago: invalid: "Nu e nimic de văzut aici" +_2fa: + renewTOTPCancel: "Nu, mulțumesc." _widgets: profile: "Profil" instanceInfo: "Informații despre instanță" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index d8f7fe5193..25f409df92 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -61,7 +61,7 @@ copyProfileUrl: "Скопировать URL профиля " searchUser: "Поиск людей" reply: "Ответ" loadMore: "Показать еще" -showMore: "Показать еще" +showMore: "Показать ещё" showLess: "Закрыть" youGotNewFollower: "Новый подписчик" receiveFollowRequest: "Получен запрос на подписку" @@ -120,7 +120,12 @@ sensitive: "Содержимое не для всех" add: "Добавить" reaction: "Реакции" reactions: "Реакции" -reactionSetting: "Реакции, отображаемые в палитре" +emojiPicker: "Палитра эмодзи" +pinnedEmojisForReactionSettingDescription: "Здесь можно закрепить эмодзи для реакций" +pinnedEmojisSettingDescription: "Здесь можно закрепить эмодзи в общей палитре" +emojiPickerDisplay: "Внешний вид палитры" +overwriteFromPinnedEmojisForReaction: "Заменить на эмодзи из списка реакций" +overwriteFromPinnedEmojis: "Заменить на эмодзи из общего списка закреплённых" reactionSettingDescription2: "Расставляйте перетаскиванием, удаляйте нажатием, добавляйте кнопкой «+»." rememberNoteVisibility: "Запоминать видимость заметок" attachCancel: "Удалить вложение" @@ -429,7 +434,6 @@ share: "Поделиться" notFound: "Не найдено" notFoundDescription: "Страница по указанной ссылке не найдена" uploadFolder: "Место загрузки по умолчанию" -cacheClear: "Очистка кэша" markAsReadAllNotifications: "Отметить все уведомления как прочитанные" markAsReadAllUnreadNotes: "Отметить все заметки как прочитанные" markAsReadAllTalkMessages: "Отметить все реплики как прочитанные" @@ -643,7 +647,7 @@ create: "Создать" notificationSetting: "Настройки уведомлений" notificationSettingDesc: "Выберите тип уведомлений для отображения" useGlobalSetting: "Использовать глобальные настройки" -useGlobalSettingDesc: "Если включено, будут использоваться настройки учётной записи. Если включить, этот виджет можно будет настроить индивидуально." +useGlobalSettingDesc: "Если включено, будут использоваться настройки учётной записи. Если отключить, этот виджет можно будет настроить индивидуально." other: "Другие" regenerateLoginToken: "Создать новый токен для входа" regenerateLoginTokenDescription: "Создаёт новый токен, используемый внутри программы во время входа. Обычно в этом нет необходимости. При создании все устройства будут отключены." @@ -681,7 +685,7 @@ createNewClip: "Новая подборка" unclip: "Убрать из подборки" confirmToUnclipAlreadyClippedNote: "Эта заметка уже есть в подборке «{name}». Удалить из этой подборки?" public: "Общедоступно" -private: "Показываются только вам" +private: "Личное" i18nInfo: "Misskey переводят на разные языки добровольцы со всего света. Ваша помощь тоже пригодится здесь: {link}." manageAccessTokens: "Управление токенами доступа" accountInfo: "Сведения об учётной записи" @@ -858,8 +862,6 @@ makeReactionsPublicDescription: "Список сделанных вами реа classic: "Классика" muteThread: "Скрыть цепочку" unmuteThread: "Отменить сокрытие цепочки" -ffVisibility: "Видимость подписок и подписчиков" -ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков." continueThread: "Показать следующие ответы" deleteAccountConfirm: "Учётная запись будет безвозвратно удалена. Подтверждаете?" incorrectPassword: "Пароль неверен." @@ -955,7 +957,7 @@ numberOfProfileView: "Количество профилей для просмо like: "Нравится!" unlike: "Отменить «нравится»" numberOfLikes: "Количество лайков" -show: "Отображение" +show: "Показать" neverShow: "Больше не показывать" remindMeLater: "Напомнить позже" didYouLikeMisskey: "Вам нравится Misskey?" @@ -1057,6 +1059,8 @@ options: "Настройки ролей" specifyUser: "Указанный пользователь" failedToPreviewUrl: "Предварительный просмотр недоступен" update: "Обновить" +rolesThatCanBeUsedThisEmojiAsReaction: "Роли тех, кому можно использовать эти эмодзи как реакцию" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Если здесь ничего не указать, в качестве реакции эту эмодзи сможет использовать каждый." later: "Позже" goToMisskey: "К Misskey" additionalEmojiDictionary: "Дополнительные словари эмодзи" @@ -1071,6 +1075,7 @@ doYouAgree: "Согласны?" icon: "Аватар" replies: "Ответы" renotes: "Репост" +loadReplies: "Показать ответы" flip: "Переворот" _initialAccountSetting: accountCreated: "Аккаунт успешно создан!" @@ -1082,6 +1087,11 @@ _initialAccountSetting: _initialTutorial: _note: description: "Посты в Misskey называются 'Заметками.' Заметки отсортированы в хронологическом порядке в ленте и обновляются в режиме реального времени." +_timelineDescription: + home: "В персональной ленте располагаются заметки тех, на которых вы подписаны." + local: "Местная лента показывает заметки всех пользователей этого сайта." + social: "В социальной ленте собирается всё, что есть в персональной и местной лентах." + global: "В глобальную ленту попадает вообще всё со связанных инстансов." _serverSettings: iconUrl: "Адрес на иконку роли" _achievements: @@ -1589,6 +1599,14 @@ _ago: monthsAgo: "{n} мес. назад" yearsAgo: "{n} г. назад" invalid: "Ничего нет" +_timeIn: + seconds: "Через {n} с" + minutes: "Через {n} мин" + hours: "Через {n} ч" + days: "Через {n} сут" + weeks: "Через {n} нед." + months: "Через {n} мес." + years: "Через {n} г." _time: second: "с" minute: "мин" @@ -1704,7 +1722,7 @@ _widgets: clicker: "Счётчик щелчков" _cw: hide: "Спрятать" - show: "Показать еще" + show: "Показать" chars: "знаков: {count}" files: "файлов: {count}" _poll: diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 3fc358f987..ccd9767695 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -113,7 +113,6 @@ sensitive: "NSFW" add: "Pridať" reaction: "Reakcie" reactions: "Reakcie" -reactionSetting: "Reakcie zobrazené vo výbere reakcií" reactionSettingDescription2: "Ťahaním preusporiadate, kliknutím odstránite, Stlačením \"+\" pridáte" rememberNoteVisibility: "Zapamätať nastavenia viditeľnosti poznámky" attachCancel: "Odstrániť prílohu" @@ -411,7 +410,6 @@ share: "Zdieľať" notFound: "Nenájdené" notFoundDescription: "Nenašla sa žiadna stránka na zadanej URL." uploadFolder: "Predvolený priečinok pre nahrávanie" -cacheClear: "Vyčistiť cache" markAsReadAllNotifications: "Označiť všetky oznámenia ako prečítané" markAsReadAllUnreadNotes: "Označiť všetky poznámky ako prečítané" markAsReadAllTalkMessages: "Označiť všetky správy ako prečítané" @@ -823,8 +821,6 @@ makeReactionsPublicDescription: "Toto spraví všetky vaše minulé reakcie vidi classic: "Klasika" muteThread: "Ztíšiť vlákno" unmuteThread: "Zrušiť stíšenie vlákna" -ffVisibility: "Viditeľnosť sledujúcich/sledovaných" -ffVisibilityDescription: "Umožňuje nastaviť kto vidí koho sledujete a kto vás sleduje." continueThread: "Zobraziť pokračovanie vlákna" deleteAccountConfirm: "Toto nezvrátiteľne vymaže váš účet. Pokračovať?" incorrectPassword: "Nesprávne heslo." diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index a1b9ef137d..4defa3b11f 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -118,7 +118,6 @@ sensitive: "Känsligt innehåll" add: "Lägg till" reaction: "Reaktioner" reactions: "Reaktioner" -reactionSetting: "Reaktioner som ska visas i reaktionsväljaren" reactionSettingDescription2: "Dra för att omordna, klicka för att radera, tryck \"+\" för att lägga till." rememberNoteVisibility: "Komihåg notvisningsinställningar" attachCancel: "Ta bort bilaga" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index efa400eced..d94cfbfc51 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -121,7 +121,6 @@ sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW add: "เพิ่ม" reaction: "รีแอคชั่น" reactions: "รีแอคชั่น" -reactionSetting: "รีแอคชั่นไปยังแสดงผลในตัวเลือกการรีแอคชั่น" reactionSettingDescription2: "กดลากเพื่อจัดลำดับใหม่ กดคลิกเพื่อลบ กด \"+\" เพื่อเพิ่ม" rememberNoteVisibility: "จดจำการตั้งค่าการมองเห็นตัวโน้ต" attachCancel: "ลบไฟล์ออกที่แนบมา" @@ -195,6 +194,7 @@ perHour: "ทุกชั่วโมง" perDay: "ต่อวัน" stopActivityDelivery: "หยุดส่งกิจกรรม" blockThisInstance: "บล็อกอินสแตนซ์นี้" +silenceThisInstance: "ปกปิดอินสแตนซ์นี้" operations: "ดำเนินการ" software: "ซอฟต์แวร์" version: "เวอร์ชั่น" @@ -214,6 +214,7 @@ clearCachedFiles: "ล้างแคช" clearCachedFilesConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะลบไฟล์ระยะไกลที่แคชไว้ทั้งหมด?" blockedInstances: "อินสแตนซ์ที่ ถูกบล็อก" blockedInstancesDescription: "ระบุชื่อโฮสต์ของอินสแตนซ์ที่คุณต้องการบล็อก อินสแตนซ์ที่อยู่ในรายการนั้นจะไม่สามารถพูดคุยกับอินสแตนซ์นี้ได้อีกต่อไป" +silencedInstances: "ปกปิดอินสแตนซ์นี้แล้ว" muteAndBlock: "ปิดเสียงและบล็อก" mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง" blockedUsers: "ผู้ใช้ที่ถูกบล็อก" @@ -434,7 +435,6 @@ share: "แชร์" notFound: "ไม่พบหน้าที่ต้องการ" notFoundDescription: "ไม่พบหน้าที่สอดคล้องตรงกันกับ URL นี้นะ" uploadFolder: "โฟลเดอร์เริ่มต้นสำหรับอัพโหลด" -cacheClear: "ล้างแคช" markAsReadAllNotifications: "ทำเครื่องหมายการแจ้งเตือนทั้งหมดว่าอ่านแล้ว" markAsReadAllUnreadNotes: "ทำเครื่องหมายโน้ตทั้งหมดว่าอ่านแล้ว" markAsReadAllTalkMessages: "ทำเครื่องหมายข้อความทั้งหมดว่าอ่านแล้ว" @@ -560,6 +560,10 @@ output: "เอาท์พุต" script: "สคริปต์" disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ" updateRemoteUser: "อัปเดตข้อมูลผู้ใช้งานระยะไกล" +unsetUserAvatar: "เลิกตั้งอวตาร" +unsetUserAvatarConfirm: "คุณแน่ใจหรือไม่ว่าต้องการเลิกตั้งอวตาร?" +unsetUserBanner: "เลิกตั้งแบนเนอร์" +unsetUserBannerConfirm: "คุณแน่ใจหรือไม่ว่าต้องการเลิกตั้งแบนเนอร์เลยมั้ย?" deleteAllFiles: "ลบไฟล์ทั้งหมด" deleteAllFilesConfirm: "นายแน่ใจแล้วหรอว่าต้องการที่จะลบไฟล์ทั้งหมด?" removeAllFollowing: "เลิกติดตามผู้ใช้ที่ติดตามทั้งหมด" @@ -631,6 +635,7 @@ smtpSecure: "ใช้โดยนัย SSL/TLS สำหรับการเ smtpSecureInfo: "ปิดสิ่งนี้เมื่อใช้ STARTTLS" testEmail: "ทดสอบการส่งอีเมล" wordMute: "ปิดเสียงคำ" +hardWordMute: "ปิดเสียงคำยาก" regexpError: "ข้อผิดพลาดของนิพจน์ทั่วไป" regexpErrorDescription: "เกิดข้อผิดพลาดในนิพจน์ทั่วไปในบรรทัดที่ {line} ของการปิดเสียงคำ {tab} ของคุณ:" instanceMute: "ปิดเสียง อินสแตนซ์" @@ -864,8 +869,6 @@ makeReactionsPublicDescription: "การทำเช่นนี้จะท classic: "คลาสสิค" muteThread: "ปิดเสียงเธรด" unmuteThread: "เปิดเสียงเธรด" -ffVisibility: "การมองเห็นผู้ติดตาม/ผู้ติดตาม" -ffVisibilityDescription: "ช่วยให้คุณสามารถกำหนดค่าได้ว่าใครสามารถดูได้ว่าคุณติดตามใครและใครติดตามคุณบ้าง" continueThread: "ดูความต่อเนื่องเธรด" deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?" incorrectPassword: "รหัสผ่านไม่ถูกต้อง" @@ -975,6 +978,7 @@ assign: "กำหนด" unassign: "ยังไม่มอบหมาย" color: "สี" manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" +manageAvatarDecorations: "จัดการตกแต่งอวตาร" youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ" cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว" cannotPerformTemporaryDescription: "ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้ง" @@ -1132,7 +1136,19 @@ impressumUrl: "URL อิมเพรสชั่น" privacyPolicy: "นโยบายความเป็นส่วนตัว" privacyPolicyUrl: "URL นโยบายความเป็นส่วนตัว" tosAndPrivacyPolicy: "เงื่อนไขในการให้บริการและนโยบายความเป็นส่วนตัว" +avatarDecorations: "การตกแต่งอวตาร" +attach: "แนบ" +detach: "นำออก" +angle: "แองเกิล" flip: "ย้อนกลับ" +showAvatarDecorations: "แสดงตกแต่งอวตาร" +releaseToRefresh: "ปล่อยเพื่อรีเฟรช" +refreshing: "กำลังรีเฟรช..." +pullDownToRefresh: "ดึงลงเพื่อรีเฟรช" +disableStreamingTimeline: "ปิดใช้งานอัปเดตไทม์ไลน์แบบเรียลไทม์" +useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว" +signupPendingError: "มีปัญหาในการตรวจสอบที่อยู่อีเมลลิงก์อาจหมดอายุแล้ว" +doReaction: "เพิ่มรีแอคชั่น" _announcement: forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น" forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน" @@ -1142,6 +1158,7 @@ _announcement: tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ" readConfirmTitle: "ทำเครื่องหมายบอกว่าอ่านแล้วเลยมั้ย?" readConfirmText: "การดำเนินการนี้จะทำเครื่องหมายเนื้อหาของ \"{title}\" บอกว่าอ่านแล้วนะ" + silence: "ไม่มีการแจ้งเตือน" _initialAccountSetting: accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!" letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ" @@ -1154,8 +1171,31 @@ _initialAccountSetting: pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ" initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!" haveFun: "ขอให้สนุก {name}!" + startTutorial: "เริ่มการฝึกสอน" skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?" laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?" +_initialTutorial: + launchTutorial: "เริ่มบทช่วยสอน" + title: "บทช่วยสอน" + wellDone: "ทำได้ดีมาก!" + skipAreYouSure: "ต้องการออกจากบทช่วยสอนใช่ไหม?" + _landing: + title: "ยินดีต้อนรับสู่บทช่วยสอน" + _note: + title: "โน้ตคืออะไร?" + _reaction: + title: "รีแอคชั่นคืออะไร?" + _timeline: + title: "แนวคิดเรื่องของไทม์ไลน์" + _postNote: + title: "ตั้งค่ากำลังโพสต์โน้ต" + _visibility: + description: "คุณสามารถจำกัดผู้ที่สามารถดูโน้ตของคุณได้นะ" + public: "โน้ตของคุณนั้นจะปรากฏแก่ผู้ใช้งานทุกคน" + _cw: + title: "คำเตือนเกี่ยวกับเนื้อหา" + _exampleNote: + cw: "นี่อาจจะทำให้คุณหิวอย่างแน่นอน!" _serverRules: description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ" _serverSettings: @@ -1464,6 +1504,7 @@ _role: inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์" inviteExpirationTime: "วันหมดอายุของรหัสการเชิญ" canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" + canManageAvatarDecorations: "จัดการตกแต่งอวตาร" driveCapacity: "ความจุของไดรฟ์" alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ" pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้" @@ -2130,3 +2171,15 @@ _fileViewer: uploadedAt: "วันที่เข้าร่วม" attachedNotes: "โน้ตที่แนบมาด้วย" thisPageCanBeSeenFromTheAuthor: "หน้าเพจนี้จะสามารถปรากฏได้โดยผู้ใช้ที่อัปโหลดไฟล์นี้เท่านั้น" +_externalResourceInstaller: + _plugin: + metaTitle: "ข้อมูลส่วนเสริม" + _theme: + metaTitle: "ข้อมูลธีม" + _vendorInfo: + title: "ข้อมูลผู้จัดจำหน่าย" + _errors: + _pluginParseFailed: + title: "ข้อผิดพลาด AiScript" + _themeParseFailed: + title: "การแยกวิเคราะห์ธีมล้มเหลว" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 3dd7a5b797..0793592d34 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -121,7 +121,6 @@ sensitive: "Hassas içerik" add: "Ekle" reaction: "Tepkiler" reactions: "Tepkiler" -reactionSetting: "Palette görünecek tepkiler" reactionSettingDescription2: "Sıralamak için sürükleyin, silmek için tıklayın, eklemek için \"+\" tuşuna tıklayın." rememberNoteVisibility: "Görünürlük ayarlarını hatırla" attachCancel: "Eki sil" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 016f41a8d6..9b609edebb 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -55,6 +55,7 @@ copyRSS: "Скопіювати RSS" copyUsername: "Скопіювати ім’я користувача" copyUserId: "Копіювати ID користувача" copyNoteId: "блокнот ID користувача" +copyFileId: "Скопіювати ідентифікатор файлу." searchUser: "Пошук користувачів" reply: "Відповісти" loadMore: "Показати більше" @@ -115,7 +116,6 @@ sensitive: "NSFW" add: "Додати" reaction: "Реакції" reactions: "Реакції" -reactionSetting: "Налаштування реакцій" reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати." rememberNoteVisibility: "Пам’ятати параметри видимісті" attachCancel: "Видалити вкладення" @@ -133,6 +133,7 @@ unblockConfirm: "Ви впевнені, що хочете розблокуват suspendConfirm: "Ви впевнені, що хочете призупинити цей акаунт?" unsuspendConfirm: "Ви впевнені, що хочете відновити цей акаунт?" selectList: "Виберіть список" +editList: "Редагувати список." selectChannel: "Виберіть канал" selectAntenna: "Виберіть антену" selectWidget: "Виберіть віджет" @@ -408,7 +409,6 @@ share: "Поділитись" notFound: "Не знайдено" notFoundDescription: "Сторінка за вказаною адресою не знайдена." uploadFolder: "Місце для завантаження за замовчуванням" -cacheClear: "Очистити кеш" markAsReadAllNotifications: "Позначити всі сповіщення як прочитані" markAsReadAllUnreadNotes: "Позначити всі нотатки як прочитані" markAsReadAllTalkMessages: "Позначити всі повідомлення як прочитані" @@ -449,6 +449,7 @@ or: "або" language: "Мова" uiLanguage: "Мова інтерфейсу" aboutX: "Про {x}" +native: "місцевий" disableDrawer: "Не використовувати висувні меню" noHistory: "Історія порожня" signinHistory: "Історія входів" @@ -527,6 +528,8 @@ output: "Вихід" script: "Скрипт" disablePagesScript: "Вимкнути AiScript на Сторінках" updateRemoteUser: "Оновити інформацію про віддаленого користувача" +unsetUserAvatar: "Деактивувати піктограму." +unsetUserBanner: "Випустити прапор." deleteAllFiles: "Видалити всі файли" deleteAllFilesConfirm: "Ви дійсно хочете видалити всі файли?" removeAllFollowing: "Скасувати всі підписки" @@ -814,7 +817,6 @@ makeReactionsPublicDescription: "Це зробить список усіх ва classic: "Класичний" muteThread: "Приглушити тред" unmuteThread: "Скасувати глушіння" -ffVisibility: "Видимість підписок/підписників" continueThread: "Показати продовження треду" deleteAccountConfirm: "Це незворотно видалить ваш акаунт. Продовжити?" incorrectPassword: "Неправильний пароль." diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index 9bf68d8a4f..3d00e47394 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -120,7 +120,6 @@ sensitive: "Sezuvchan" add: "Qo'shish" reaction: "Reaktsiyalar" reactions: "Reaktsiyalar" -reactionSetting: "Reaksiyalar ro'yxati" reactionSettingDescription2: "Qayta tartiblash uchun ushlab turib siljiting, oʻchirish uchun bosing, qoʻshish uchun “+” tugmasini bosing." rememberNoteVisibility: "Qaydning ko'rinish sozlamarini eslab qolish" attachCancel: "Qo'shimchani olib tashlash" @@ -428,7 +427,6 @@ share: "Yuborish" notFound: "Topilmadi" notFoundDescription: "Ushbu sahifa topilmadi" uploadFolder: "Jildni yuklash" -cacheClear: "Keshni tozalash" markAsReadAllNotifications: "Bildirishnomalarni o'qilgan deb belgilash" markAsReadAllUnreadNotes: "Barch xabarlarni oq'ilgan deb belgilash" markAsReadAllTalkMessages: "Barcha suhbatlarni o'qilgan deb belgilang" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index ac9f55c5de..e4f7d27423 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -121,7 +121,6 @@ sensitive: "Nhạy cảm" add: "Thêm" reaction: "Biểu cảm" reactions: "Biểu cảm" -reactionSetting: "Chọn những biểu cảm hiển thị" reactionSettingDescription2: "Kéo để sắp xếp, nhấn để xóa, nhấn \"+\" để thêm." rememberNoteVisibility: "Lưu kiểu tút mặc định" attachCancel: "Gỡ tập tin đính kèm" @@ -433,7 +432,6 @@ share: "Chia sẻ" notFound: "Không tìm thấy" notFoundDescription: "Không tìm thấy trang nào tương ứng với URL này." uploadFolder: "Thư mục tải lên mặc định" -cacheClear: "Xóa bộ nhớ đệm" markAsReadAllNotifications: "Đánh dấu tất cả các thông báo là đã đọc" markAsReadAllUnreadNotes: "Đánh dấu tất cả các tút là đã đọc" markAsReadAllTalkMessages: "Đánh dấu tất cả các tin nhắn là đã đọc" @@ -859,8 +857,6 @@ makeReactionsPublicDescription: "Điều này sẽ hiển thị công khai danh classic: "Cổ điển" muteThread: "Không quan tâm nữa" unmuteThread: "Quan tâm tút này" -ffVisibility: "Hiển thị Theo dõi/Người theo dõi" -ffVisibilityDescription: "Quyết định ai có thể xem những người bạn theo dõi và những người theo dõi bạn." continueThread: "Tiếp tục xem chuỗi tút" deleteAccountConfirm: "Điều này sẽ khiến tài khoản bị xóa vĩnh viễn. Vẫn tiếp tục?" incorrectPassword: "Sai mật khẩu." diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 76bc5c3271..bfacc03e0a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -121,7 +121,6 @@ sensitive: "敏感内容" add: "添加" reaction: "回应" reactions: "回应" -reactionSetting: "在选择器中显示回应" reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。" rememberNoteVisibility: "保存上次设置的可见性" attachCancel: "删除附件" @@ -311,6 +310,7 @@ folderName: "文件夹名称" createFolder: "创建文件夹" renameFolder: "重命名文件夹" deleteFolder: "删除文件夹" +folder: "文件夹" addFile: "添加文件" emptyDrive: "网盘中无文件" emptyFolder: "此文件夹中无文件" @@ -437,7 +437,6 @@ share: "分享" notFound: "未找到" notFoundDescription: "没有与指定 URL 对应的页面。" uploadFolder: "默认上传文件夹" -cacheClear: "清空缓存" markAsReadAllNotifications: "将所有通知标为已读" markAsReadAllUnreadNotes: "将所有帖子标记为已读" markAsReadAllTalkMessages: "将所有聊天标记为已读" @@ -867,8 +866,6 @@ makeReactionsPublicDescription: "将您发表过的回应设置成公开可见 classic: "经典" muteThread: "屏蔽帖子列表" unmuteThread: "取消屏蔽帖子列表" -ffVisibility: "关注关系的可见范围" -ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围" continueThread: "查看更多帖子" deleteAccountConfirm: "将要删除账户。是否确认?" incorrectPassword: "密码错误" @@ -1131,6 +1128,7 @@ mutualFollow: "互相关注" fileAttachedOnly: "仅限媒体" showRepliesToOthersInTimeline: "在时间线上显示给其他人的回复" hideRepliesToOthersInTimeline: "在时间线上隐藏给其他人的回复" +avatarDecorations: "头像挂件" flip: "翻转" _announcement: forExistingUsers: "仅限现有用户" @@ -1163,7 +1161,7 @@ _serverSettings: appIconUsageExample: "例如:作为书签添加到 PWA 或手机主屏幕的时候" appIconStyleRecommendation: "因为有可能会被裁切为圆形或者圆角矩形,建议使用边缘带有留白背景的图标。" appIconResolutionMustBe: "分辨率必须为 {resolution}。" - manifestJsonOverride: "覆盖 mainfest.json" + manifestJsonOverride: "覆盖 manifest.json" shortName: "简称" shortNameDescription: "如果服务器的正式名称很长,可以用简称或者別名来替代。" _accountMigration: @@ -1706,6 +1704,9 @@ _ago: monthsAgo: "{n} 月前" yearsAgo: "{n} 年前" invalid: "没有" +_timeIn: + seconds: "{n}秒后" + days: "{n}天后" _time: second: "秒" minute: "分" @@ -2123,3 +2124,7 @@ _moderationLogTypes: _fileViewer: url: "URL" uploadedAt: "添加日期" +_externalResourceInstaller: + _errors: + _pluginParseFailed: + title: "AiScript 错误" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index ad0741693f..36b6e77e9b 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -121,7 +121,12 @@ sensitive: "敏感內容" add: "新增" reaction: "反應" reactions: "反應" -reactionSetting: "在選擇器中顯示反應" +emojiPicker: "表情符號選擇器" +pinnedEmojisForReactionSettingDescription: "選擇反應時可以設定要固定顯示在頂端的表情符號" +pinnedEmojisSettingDescription: "輸入表情符號時可以設定要固定顯示在頂端的表情符號" +emojiPickerDisplay: "顯示表情符號選擇器" +overwriteFromPinnedEmojisForReaction: "從反應複寫設定" +overwriteFromPinnedEmojis: "從一般複寫設定" reactionSettingDescription2: "拖動以交換,點擊以刪除,按下「+」以新增。" rememberNoteVisibility: "記住貼文可見性" attachCancel: "移除附件" @@ -261,6 +266,7 @@ removed: "已刪除" removeAreYouSure: "確定要刪掉「{x}」嗎?" deleteAreYouSure: "確定要刪掉「{x}」嗎?" resetAreYouSure: "確定要重設嗎?" +areYouSure: "是否確定?" saved: "已儲存" messaging: "聊天" upload: "上傳" @@ -292,13 +298,13 @@ birthday: "生日" yearsOld: "{age} 歲" registeredDate: "註冊日期" location: "位置" -theme: "外觀主題" -themeForLightMode: "在淺色模式下使用的主題" -themeForDarkMode: "在深色模式下使用的主題" +theme: "佈景主題" +themeForLightMode: "在淺色模式下使用的佈景主題" +themeForDarkMode: "在深色模式下使用的佈景主題" light: "淺色" dark: "深色" -lightThemes: "淺色主題" -darkThemes: "深色主題" +lightThemes: "淺色佈景主題" +darkThemes: "深色佈景主題" syncDeviceDarkMode: "與設備的深色模式同步" drive: "雲端硬碟" fileName: "檔案名稱" @@ -311,6 +317,7 @@ folderName: "資料夾名稱" createFolder: "新增資料夾" renameFolder: "重新命名資料夾" deleteFolder: "刪除資料夾" +folder: "資料夾" addFile: "加入附件" emptyDrive: "雲端硬碟為空" emptyFolder: "資料夾為空" @@ -437,7 +444,6 @@ share: "分享" notFound: "查無項目" notFoundDescription: "查無此頁" uploadFolder: "預設上傳資料夾" -cacheClear: "清除快取" markAsReadAllNotifications: "標記所有通知為已讀" markAsReadAllUnreadNotes: "標記所有貼文為已讀" markAsReadAllTalkMessages: "標記所有訊息為已讀" @@ -544,6 +550,8 @@ showInPage: "在頁面中顯示" popout: "彈出式視窗" volume: "音量" masterVolume: "主音量" +notUseSound: "關閉音效" +useSoundOnlyWhenActive: "瀏覽器在前景運作時,Misskey 才會發出音效" details: "詳細資訊" chooseEmoji: "選擇您的表情符號" unableToProcess: "操作無法完成" @@ -564,6 +572,10 @@ output: "輸出" script: "腳本" disablePagesScript: "停用頁面的 AiScript 腳本" updateRemoteUser: "更新遠端使用者資訊" +unsetUserAvatar: "移除使用者的大頭貼" +unsetUserAvatarConfirm: "確定要移除使用者的大頭貼嗎?" +unsetUserBanner: "移除使用者的橫幅圖像" +unsetUserBannerConfirm: "確定要移除使用者的橫幅圖像嗎?" deleteAllFiles: "刪除所有檔案" deleteAllFilesConfirm: "要刪除所有檔案嗎?" removeAllFollowing: "解除所有追隨" @@ -589,12 +601,12 @@ deletedNote: "已刪除的貼文" invisibleNote: "私密的貼文" enableInfiniteScroll: "啟用自動滾動頁面模式" visibility: "可見性" -poll: "投票" +poll: "票選活動" useCw: "隱藏內容" enablePlayer: "開啟播放器" disablePlayer: "關閉播放器" expandTweet: "展開推文" -themeEditor: "主題編輯器" +themeEditor: "佈景主題編輯器" description: "描述" describeFile: "新增標題" enterFileDescription: "輸入標題" @@ -620,11 +632,11 @@ tokenRequested: "允許存取帳戶" pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。" notificationType: "通知形式" edit: "編輯" -emailServer: "電郵伺服器" -enableEmail: "啟用發送電郵功能" -emailConfigInfo: "用於確認電郵地址及密碼重置" +emailServer: "電子郵件伺服器" +enableEmail: "啟用發送電子郵件功能" +emailConfigInfo: "用於確認電子郵件地址及密碼重置" email: "電子郵件" -emailAddress: "電郵地址" +emailAddress: "電子郵件位址" smtpConfig: "SMTP 伺服器設定" smtpHost: "主機" smtpPort: "埠" @@ -635,6 +647,7 @@ smtpSecure: "在 SMTP 連接中使用隱式 SSL/TLS" smtpSecureInfo: "使用 STARTTLS 時關閉。" testEmail: "測試郵件發送" wordMute: "被靜音的文字" +hardWordMute: "硬文字靜音" regexpError: "正規表達式錯誤" regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:" instanceMute: "被靜音的實例" @@ -718,7 +731,7 @@ disableShowingAnimatedImages: "不播放動態圖檔" highlightSensitiveMedia: "強調敏感標記" verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。" notSet: "未設定" -emailVerified: "已成功驗證您的電郵" +emailVerified: "已成功驗證您的電子郵件地址" noteFavoritesCount: "我的最愛貼文的數目" pageLikesCount: "頁面被按讚次數" pageLikedCount: "頁面被按讚次數" @@ -770,11 +783,11 @@ capacity: "容量" inUse: "已使用" editCode: "編輯代碼" apply: "套用" -receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知" +receiveAnnouncementFromInstance: "接收來自伺服器的通知" emailNotification: "郵件通知" publish: "發布" inChannelSearch: "頻道内搜尋" -useReactionPickerForContextMenu: "點擊右鍵開啟反應工具欄" +useReactionPickerForContextMenu: "點擊右鍵開啟反應選擇器" typingUsers: "{users}輸入中" jumpToSpecifiedDate: "跳轉到特定日期" showingPastTimeline: "顯示過往的時間軸" @@ -831,7 +844,7 @@ previewNoteText: "預覽文本" customCss: "自定義 CSS" customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能導致客戶端無法正常使用。" global: "全域" -squareAvatars: "頭像以方形顯示" +squareAvatars: "大頭貼以方形顯示" sent: "發送" received: "收取" searchResult: "搜尋結果" @@ -868,8 +881,8 @@ makeReactionsPublicDescription: "將您做過的反應設為公開可見。" classic: "經典" muteThread: "將貼文串設為靜音" unmuteThread: "將貼文串的靜音解除" -ffVisibility: "連繫的可見性" -ffVisibilityDescription: "您可以設定追隨或追隨者資訊的公開範圍" +followingVisibility: "追隨中的可見性" +followersVisibility: "追隨者的可見性" continueThread: "查看更多貼文" deleteAccountConfirm: "將要刪除帳戶。是否確定?" incorrectPassword: "密碼錯誤。" @@ -882,13 +895,13 @@ overridedDeviceKind: "裝置類型" smartphone: "智慧型手機" tablet: "平板" auto: "自動" -themeColor: "主題顏色" +themeColor: "佈景主題顏色" size: "大小" numberOfColumn: "列數" searchByGoogle: "搜尋" -instanceDefaultLightTheme: "實例預設的淺色主題" -instanceDefaultDarkTheme: "實例預設的深色主題" -instanceDefaultThemeDescription: "輸入物件形式的主題代碼" +instanceDefaultLightTheme: "實例預設的淺色佈景主題" +instanceDefaultDarkTheme: "實例預設的深色佈景主題" +instanceDefaultThemeDescription: "輸入物件形式的佈景主題代碼" mutePeriod: "靜音的期限" period: "期限" indefinitely: "無期限" @@ -942,7 +955,7 @@ cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制 beta: "測試版" enableAutoSensitive: "自動 NSFW 判定" enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依實例規則而自動啟用。" -activeEmailValidationDescription: "積極驗證使用者的電郵地址,以判斷它是否可以通訊。關閉此選項代表只會檢查地址是否符合格式。" +activeEmailValidationDescription: "主動地驗證使用者的電子郵件地址,以確定是否是一次性地址以及是否可以真正與其進行通訊。關閉時,僅檢查格式是否正確。" navbar: "導覽列" shuffle: "隨機" account: "帳戶" @@ -1020,6 +1033,8 @@ resetPasswordConfirm: "重設密碼?" sensitiveWords: "敏感詞" sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。" sensitiveWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。" +hiddenTags: "隱藏標籤" +hiddenTagsDescription: "設定的標籤不會在趨勢中顯示,換行可以設定多個標籤。" notesSearchNotAvailable: "無法使用搜尋貼文功能。" license: "授權" unfavoriteConfirm: "要取消收錄我的最愛嗎?" @@ -1032,6 +1047,7 @@ enableChartsForRemoteUser: "生成遠端使用者的圖表" enableChartsForFederatedInstances: "生成遠端伺服器的圖表" showClipButtonInNoteFooter: "新增摘錄按鈕至貼文" reactionsDisplaySize: "反應的顯示尺寸" +limitWidthOfReaction: "限制反應的最大寬度,並縮小顯示尺寸。" noteIdOrUrl: "貼文ID或URL" video: "影片" videos: "影片" @@ -1147,6 +1163,7 @@ tosAndPrivacyPolicy: "服務條款和隱私政策" avatarDecorations: "頭像裝飾" attach: "裝上" detach: "取下" +detachAll: "移除所有裝飾" angle: "角度" flip: "翻轉" showAvatarDecorations: "顯示頭像裝飾" @@ -1158,6 +1175,14 @@ useGroupedNotifications: "分組顯示通知訊息" signupPendingError: "驗證您的電子郵件地址時出現問題。連結可能已過期。" cwNotationRequired: "如果開啟「隱藏內容」,則需要註解說明。" doReaction: "做出反應" +code: "程式碼" +reloadRequiredToApplySettings: "需要重新載入頁面設定才能生效。" +remainingN: "剩餘:{n}" +overwriteContentConfirm: "確定要覆蓋目前的內容嗎?" +seasonalScreenEffect: "隨季節變換畫面的呈現" +decorate: "設置頭像裝飾" +addMfmFunction: "插入MFM功能語法" +enableQuickAddMfmFunction: "顯示高級MFM選擇器" _announcement: forExistingUsers: "僅限既有的使用者" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" @@ -1194,7 +1219,7 @@ _initialTutorial: skipAreYouSure: "結束教學模式?" _landing: title: "歡迎使用本教學課程" - description: "在這裡您可以查看Misskey的基本使用方法和功能。" + description: "在這裡您可以查看 Misskey 的基本使用方法和功能。" _note: title: "什麼是貼文?" description: "在Misskey上發布的內容稱為「貼文」。貼文在時間軸上按時間順序排列,並即時更新。" @@ -1538,7 +1563,9 @@ _role: assignTarget: "指派目標" descriptionOfAssignTarget: "手動是以手動管理這個角色包含的人員。\n符合條件是設定條件以自動包含符合條件的使用者。" manual: "手動" + manualRoles: "手動角色" conditional: "符合條件" + conditionalRoles: "有條件的角色" condition: "條件" isConditionalRole: "這是條件角色。" isPublic: "角色為公開" @@ -1587,6 +1614,7 @@ _role: canHideAds: "不顯示廣告" canSearchNotes: "可否搜尋貼文" canUseTranslator: "使用翻譯功能" + avatarDecorationLimit: "頭像裝飾的最大設置量" _condition: isLocal: "本地使用者" isRemote: "遠端使用者" @@ -1615,6 +1643,7 @@ _emailUnavailable: disposable: "不是永久可用的地址" mx: "郵件伺服器不正確" smtp: "郵件伺服器沒有應答" + banned: "無法使用此電子郵件地址註冊" _ffVisibility: public: "公開" followers: "只有關注您的使用者能看到" @@ -1732,17 +1761,17 @@ _instanceMute: title: "將隱藏被設定的實例貼文。" heading: "將實例靜音" _theme: - explore: "取得佈景主題" + explore: "探索佈景主題" install: "安裝佈景主題" - manage: "佈景主題管理員" - code: "主題代碼" + manage: "管理佈景主題" + code: "佈景主題代碼" description: "描述" installed: "{name}已安裝" - installedThemes: "已經安裝的主題" - builtinThemes: "標準主題" - alreadyInstalled: "此主題已經安裝" - invalid: "主題格式錯誤" - make: "製作主題" + installedThemes: "已經安裝的佈景主題" + builtinThemes: "標準佈景主題" + alreadyInstalled: "已安裝此佈景主題" + invalid: "佈景主題格式錯誤" + make: "製作佈景主題" base: "基於" addConstant: "添加常數" constant: "常數" @@ -1759,7 +1788,7 @@ _theme: darken: "暗度" lighten: "亮度" inputConstantName: "請輸入常數名稱" - importInfo: "您可以在此貼上主題代碼,將其匯入編輯器中" + importInfo: "您可以在此貼上佈景主題代碼,將其匯入編輯器中" deleteConstantConfirm: "確定要刪除常數{const}嗎?" keys: accent: "重點色彩" @@ -1808,6 +1837,14 @@ _sfx: notification: "通知" antenna: "天線接收" channel: "頻道通知" + reaction: "選擇反應時" +_soundSettings: + driveFile: "使用雲端硬碟的音效檔案" + driveFileWarn: "請選擇雲端硬碟中的檔案" + driveFileTypeWarn: "不支援此檔案" + driveFileTypeWarnDescription: "請選擇音效檔案" + driveFileDurationWarn: "音效太長了" + driveFileDurationWarnDescription: "使用長音效檔可能會影響 Misskey 的使用體驗。仍要使用此檔案嗎?" _ago: future: "未來" justNow: "剛剛" @@ -1952,6 +1989,7 @@ _widgets: _userList: chooseList: "選擇清單" clicker: "點擊器" + birthdayFollowings: "今天生日的使用者" _cw: hide: "隱藏" show: "顯示內容" @@ -1961,7 +1999,7 @@ _poll: noOnlyOneChoice: "需要至少兩個選項。" choiceN: "選項 {n}" noMore: "沒辦法再添加選項了" - canMultipleVote: "可以多次投票" + canMultipleVote: "允許複選" expiration: "期限" infinite: "無期限" at: "結束時間" @@ -1970,7 +2008,7 @@ _poll: deadlineTime: "小時" duration: "時長" votesCount: "{n} 票" - totalVotes: "合共 {n} 票" + totalVotes: "一共{n}票" vote: "投票" showResult: "顯示結果" voted: "已投票" @@ -2014,6 +2052,7 @@ _profile: changeAvatar: "更換大頭貼" changeBanner: "變更橫幅圖像" verifiedLinkDescription: "如果輸入包含您個人資料的網站 URL,欄位旁邊將出現驗證圖示。" + avatarDecorationMax: "最多可以設置 {max} 個裝飾。" _exportOrImport: allNotes: "所有貼文" favoritedNotes: "「我的最愛」貼文" @@ -2028,7 +2067,7 @@ _charts: federation: "聯邦宇宙" apRequest: "請求" usersIncDec: "使用者增減" - usersTotal: "使用者合共" + usersTotal: "使用者總數" activeUsers: "活躍使用者" notesIncDec: "貼文増減" localNotesIncDec: "本地貼文増減" @@ -2135,6 +2174,7 @@ _notification: pollEnded: "問卷調查已產生結果" newNote: "新的貼文" unreadAntennaNote: "天線 {name}" + roleAssigned: "已授予角色" emptyPushNotificationMessage: "推送通知已更新" achievementEarned: "獲得成就" testNotification: "通知測試" @@ -2156,6 +2196,7 @@ _notification: pollEnded: "問卷調查結束" receiveFollowRequest: "已收到追隨請求" followRequestAccepted: "追隨請求已接受" + roleAssigned: "已授予角色" achievementEarned: "獲得成就" app: "應用程式通知" _actions: @@ -2250,6 +2291,8 @@ _moderationLogTypes: createAvatarDecoration: "建立頭像裝飾" updateAvatarDecoration: "更新頭像裝飾" deleteAvatarDecoration: "刪除頭像裝飾" + unsetUserAvatar: "移除使用者的大頭貼" + unsetUserBanner: "移除使用者的橫幅圖像" _fileViewer: title: "檔案詳細資訊" type: "檔案類型 " @@ -2265,8 +2308,8 @@ _externalResourceInstaller: title: "要安裝此外掛嘛?" metaTitle: "外掛資訊" _theme: - title: "要安裝此外觀主題嘛?" - metaTitle: "外觀主題資訊" + title: "要安裝此佈景主題嗎?" + metaTitle: "佈景主題資訊" _meta: base: "基本配色方案" _vendorInfo: @@ -2294,8 +2337,21 @@ _externalResourceInstaller: title: "外掛安裝失敗" description: "安裝插件時出現問題。請再試一次。請參閱 Javascript 控制台以取得錯誤詳細資訊。" _themeParseFailed: - title: "外觀主題解析錯誤" - description: "已取得資料但解析外觀主題時發生錯誤,導致無法載入。請聯絡主題作者。請檢查 Javascript 控制台以取得錯誤詳細資訊。" + title: "佈景主題解析錯誤" + description: "已取得資料但解析佈景主題時發生錯誤,導致無法載入。請聯絡佈景主題的作者。請檢查 Javascript 控制台以取得錯誤詳細資訊。" _themeInstallFailed: - title: "無法安裝外觀主題" - description: "安裝外觀主題時出現問題。請再試一次。請參閱 Javascript 控制台以取得錯誤詳細資訊。" + title: "無法安裝佈景主題" + description: "安裝佈景主題時出現問題。請再試一次。請參閱 Javascript 控制台以取得錯誤詳細資訊。" +_dataSaver: + _media: + title: "載入媒體檔案" + description: "防止自動載入圖片和影片。點擊隱藏的圖片/影片即可載入。" + _avatar: + title: "大頭貼" + description: "停止顯示大頭貼的動畫。由於動畫圖片的檔案大小可能比普通圖片大,這可以進一步減少資料流量。" + _urlPreview: + title: "網址預覽縮圖" + description: "將不再自動載入網址預覽縮圖。" + _code: + title: "程式碼突出顯示" + description: "如果使用了 MFM 的程式碼突顯標記,則在點擊之前不會載入。程式碼突顯要求加載每種程式語言的突顯定義檔案,但由於這些檔案不再自動載入,因此有望減少資料流量。" diff --git a/package.json b/package.json index 01bc4804bc..3aae34b8bf 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "codename": "shonk", "repository": { "type": "git", - "url": "https://github.com/transfem-org/sharkey.git" + "url": "https://git.joinsharkey.org/Sharkey/Sharkey.git" }, - "packageManager": "pnpm@8.10.5", + "packageManager": "pnpm@8.12.1", "workspaces": [ "packages/frontend", "packages/backend", @@ -18,6 +18,7 @@ "build-assets": "node ./scripts/build-assets.mjs", "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build-storybook": "pnpm --filter frontend build-storybook", + "build-misskey-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "init": "pnpm migrate", @@ -26,7 +27,7 @@ "check:connect": "cd packages/backend && pnpm check:connect", "migrateandstart": "pnpm migrate && pnpm start", "watch": "pnpm dev", - "dev": "node ./scripts/dev.mjs", + "dev": "node scripts/dev.mjs", "lint": "pnpm -r lint", "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "pnpm cypress run", @@ -45,18 +46,19 @@ }, "dependencies": { "execa": "8.0.1", - "cssnano": "6.0.1", + "cssnano": "6.0.2", "js-yaml": "4.1.0", - "postcss": "8.4.31", - "terser": "5.24.0", - "typescript": "5.2.2" + "postcss": "8.4.32", + "terser": "5.26.0", + "typescript": "5.3.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "6.11.0", - "@typescript-eslint/parser": "6.11.0", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", "cross-env": "7.0.3", - "cypress": "13.5.1", - "eslint": "8.53.0", - "start-server-and-test": "2.0.3" + "cypress": "13.6.1", + "eslint": "8.56.0", + "start-server-and-test": "2.0.3", + "ncp": "2.0.0" } } diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc index d9f047b6ac..0504a2d389 100644 --- a/packages/backend/.swcrc +++ b/packages/backend/.swcrc @@ -11,7 +11,7 @@ "decoratorMetadata": true }, "experimental": { - "keepImportAttributes": true + "keepImportAssertions": true }, "baseUrl": "src", "paths": { diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg new file mode 100644 index 0000000000..9d21137072 --- /dev/null +++ b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf new file mode 100644 index 0000000000..a2601e0f1b Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf differ diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff new file mode 100644 index 0000000000..d9f471fa35 Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff differ diff --git a/packages/backend/assets/fonts/sharkey-icons/style.css b/packages/backend/assets/fonts/sharkey-icons/style.css new file mode 100644 index 0000000000..7fb0f94504 --- /dev/null +++ b/packages/backend/assets/fonts/sharkey-icons/style.css @@ -0,0 +1,31 @@ +@charset "UTF-8"; + +@font-face { + font-family: "custom-sharkey-icons"; + src: url("./custom-sharkey-icons.woff") format("woff"), + url("./custom-sharkey-icons.ttf") format("truetype"), + url("./custom-sharkey-icons.svg#custom-sharkey-icons") format("svg"); + font-weight: normal; + font-style: normal; + font-display: block; +} + +.sk-icons { + font-family: "custom-sharkey-icons" !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + speak: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.sk-icons.sk-shark:before { + content: "\61"; +} + +.sk-icons.sk-misskey:before { + content: "\62"; +} diff --git a/packages/backend/generate_api_json.js b/packages/backend/generate_api_json.js new file mode 100644 index 0000000000..5819c60a5f --- /dev/null +++ b/packages/backend/generate_api_json.js @@ -0,0 +1,8 @@ +import { loadConfig } from './built/config.js' +import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js' +import { writeFileSync } from "node:fs"; + +const config = loadConfig(); +const spec = genOpenapiSpec(config); + +writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8'); \ No newline at end of file diff --git a/packages/backend/migration/1557761316509-AddSomeUrls.js b/packages/backend/migration/1557761316509-AddSomeUrls.js index 0eef193281..b83ce2ed5e 100644 --- a/packages/backend/migration/1557761316509-AddSomeUrls.js +++ b/packages/backend/migration/1557761316509-AddSomeUrls.js @@ -6,8 +6,8 @@ export class AddSomeUrls1557761316509 { async up(queryRunner) { await queryRunner.query(`ALTER TABLE "meta" ADD "ToSUrl" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "repositoryUrl" character varying(512) NOT NULL DEFAULT 'https://github.com/transfem-org/sharkey'`); - await queryRunner.query(`ALTER TABLE "meta" ADD "feedbackUrl" character varying(512) DEFAULT 'https://github.com/transfem-org/sharkey/issues/new'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "repositoryUrl" character varying(512) NOT NULL DEFAULT 'https://git.joinsharkey.org/Sharkey/Sharkey'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "feedbackUrl" character varying(512) DEFAULT 'https://git.joinsharkey.org/Sharkey/Sharkey/issues/new/choose'`); } async down(queryRunner) { await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "feedbackUrl"`); diff --git a/packages/backend/migration/1700303245007-supportVerifyMailApi.js b/packages/backend/migration/1700303245007-supportVerifyMailApi.js new file mode 100644 index 0000000000..3ac59ec37a --- /dev/null +++ b/packages/backend/migration/1700303245007-supportVerifyMailApi.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SupportVerifyMailApi1700303245007 { + name = 'SupportVerifyMailApi1700303245007' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "verifymailAuthKey" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "enableVerifymailApi" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableVerifymailApi"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "verifymailAuthKey"`); + } +} diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js new file mode 100644 index 0000000000..afd3247f5c --- /dev/null +++ b/packages/backend/migration/1700383825690-hard-mute.js @@ -0,0 +1,11 @@ +export class HardMute1700383825690 { + name = 'HardMute1700383825690' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`); + } +} diff --git a/packages/backend/migration/1700902349231-add-bday-index.js b/packages/backend/migration/1700902349231-add-bday-index.js new file mode 100644 index 0000000000..251526fc26 --- /dev/null +++ b/packages/backend/migration/1700902349231-add-bday-index.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddBdayIndex1700902349231 { + name = 'AddBdayIndex1700902349231' + + async up(queryRunner) { + await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (SUBSTR("birthday", 6, 5))`); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`); + } +} diff --git a/packages/backend/migration/1701647674000-BubbleInstances.js b/packages/backend/migration/1701647674000-BubbleInstances.js new file mode 100644 index 0000000000..9928b4c36e --- /dev/null +++ b/packages/backend/migration/1701647674000-BubbleInstances.js @@ -0,0 +1,11 @@ +export class BubbleInstances1701647674000 { + name = 'BubbleInstances1701647674000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "bubbleInstances" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "bubbleInstances"`); + } +} diff --git a/packages/backend/migration/1701809447000-NSFW-Instance.js b/packages/backend/migration/1701809447000-NSFW-Instance.js new file mode 100644 index 0000000000..882aa9865d --- /dev/null +++ b/packages/backend/migration/1701809447000-NSFW-Instance.js @@ -0,0 +1,11 @@ +export class NSFWInstance1701809447000 { + name = 'NSFWInstance1701809447000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" ADD "isNSFW" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "isNSFW"`); + } +} diff --git a/packages/backend/migration/1702718871541-ffVisibility.js b/packages/backend/migration/1702718871541-ffVisibility.js new file mode 100644 index 0000000000..e9e820c897 --- /dev/null +++ b/packages/backend/migration/1702718871541-ffVisibility.js @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ffVisibility1702718871541 { + constructor() { + this.name = 'ffVisibility1702718871541'; + } + async up(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_followingvisibility_enum" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum") WITH INOUT AS ASSIGNMENT`); + await queryRunner.query(`CREATE TYPE "public"."user_profile_followersVisibility_enum" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum") WITH INOUT AS ASSIGNMENT`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "followingVisibility" "public"."user_profile_followingvisibility_enum" NOT NULL DEFAULT 'public'`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "followersVisibility" "public"."user_profile_followersVisibility_enum" NOT NULL DEFAULT 'public'`); + await queryRunner.query(`UPDATE "user_profile" SET "followingVisibility" = "ffVisibility"`); + await queryRunner.query(`UPDATE "user_profile" SET "followersVisibility" = "ffVisibility"`); + await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum")`); + await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum")`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`); + } + async down(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`); + + await queryRunner.query(`CREATE CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum") WITH INOUT AS ASSIGNMENT`); + await queryRunner.query(`UPDATE "user_profile" SET "ffVisibility" = "followingVisibility"`); + await queryRunner.query(`DROP CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum")`); + + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followersVisibility"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followingVisibility"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_followersVisibility_enum"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_followingvisibility_enum"`); + } +} diff --git a/packages/backend/migration/1703209889304-bannedEmailDomains.js b/packages/backend/migration/1703209889304-bannedEmailDomains.js new file mode 100644 index 0000000000..5dc99c138f --- /dev/null +++ b/packages/backend/migration/1703209889304-bannedEmailDomains.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class bannedEmailDomains1703209889304 { + constructor() { + this.name = 'bannedEmailDomains1703209889304'; + } + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "bannedEmailDomains" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "bannedEmailDomains"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 22030d168d..2aa10b1c96 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,11 +4,11 @@ "private": true, "type": "module", "engines": { - "node": ">=18.16.0" + "node": ">=20.10.0" }, "scripts": { - "start": "node ./built/index.js", - "start:test": "NODE_ENV=test node ./built/index.js", + "start": "node ./built/boot/entry.js", + "start:test": "NODE_ENV=test node ./built/boot/entry.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js", "revert": "pnpm typeorm migration:revert -d ormconfig.js", "check:connect": "node ./check_connect.js", @@ -16,6 +16,8 @@ "watch:swc": "swc src -d built -D -w", "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", + "restart": "pnpm build && pnpm start", + "dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"", "typecheck": "pnpm --filter megalodon build && tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.ts\"", "lint": "pnpm typecheck && pnpm eslint", @@ -23,7 +25,8 @@ "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit", "jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "test": "pnpm jest", - "test-and-coverage": "pnpm jest-and-coverage" + "test-and-coverage": "pnpm jest-and-coverage", + "generate-api-json": "node ./generate_api_json.js" }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", @@ -57,27 +60,29 @@ "dependencies": { "@aws-sdk/client-s3": "3.412.0", "@aws-sdk/lib-storage": "3.412.0", - "@bull-board/api": "5.9.1", - "@bull-board/fastify": "5.9.1", - "@bull-board/ui": "5.9.1", - "@discordapp/twemoji": "14.1.2", - "@fastify/accepts": "4.2.0", + "@bull-board/api": "5.10.2", + "@bull-board/fastify": "5.10.2", + "@bull-board/ui": "5.10.2", + "@discordapp/twemoji": "15.0.2", + "@fastify/accepts": "4.3.0", "@fastify/cookie": "9.2.0", - "@fastify/cors": "8.4.1", + "@fastify/cors": "8.5.0", "@fastify/express": "2.3.0", "@fastify/http-proxy": "9.3.0", "@fastify/multipart": "8.0.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@nestjs/common": "10.2.8", - "@nestjs/core": "10.2.8", - "@nestjs/testing": "10.2.8", + "@nestjs/common": "10.2.10", + "@nestjs/core": "10.2.10", + "@nestjs/testing": "10.2.10", "@peertube/http-signature": "1.7.0", + "@sharkey/sfm-js": "0.24.3", "@simplewebauthn/server": "8.3.5", "@sinonjs/fake-timers": "11.2.2", - "@smithy/node-http-handler": "2.1.5", + "@smithy/node-http-handler": "2.1.10", "@swc/cli": "0.1.63", - "@swc/core": "1.3.96", + "@swc/core": "1.3.100", + "@twemoji/parser": "15.0.0", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "6.0.1", @@ -86,7 +91,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "4.13.3", + "bullmq": "4.15.4", "cacheable-lookup": "7.0.0", "cbor": "9.0.1", "chalk": "5.3.0", @@ -97,14 +102,15 @@ "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify-multer": "^2.0.3", "fastify": "4.24.3", + "fastify-multer": "^2.0.3", "fastify-raw-body": "4.3.0", "feed": "4.2.2", "file-type": "18.7.0", "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", - "got": "13.0.0", + "glob": "10.3.10", + "got": "14.0.0", "happy-dom": "10.0.3", "hpagent": "1.2.0", "http-link-header": "1.1.1", @@ -113,18 +119,17 @@ "ipaddr.js": "2.1.0", "is-svg": "5.0.0", "js-yaml": "4.1.0", - "jsdom": "22.1.0", + "jsdom": "23.0.1", "json5": "2.2.3", - "jsonld": "8.3.1", - "jsrsasign": "10.8.6", + "jsonld": "8.3.2", + "jsrsasign": "10.9.0", + "meilisearch": "0.36.0", "megalodon": "workspace:*", - "meilisearch": "0.35.0", - "mfm-js": "0.23.3", - "microformats-parser": "1.5.2", + "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", "ms": "3.0.0-canary.1", - "nanoid": "5.0.3", + "nanoid": "5.0.4", "nested-property": "4.0.0", "node-fetch": "3.3.2", "nodemailer": "6.9.7", @@ -132,7 +137,7 @@ "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.2.0", + "otpauth": "9.2.1", "parse5": "7.1.2", "pg": "8.11.3", "pkce-challenge": "4.0.1", @@ -144,9 +149,9 @@ "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.20.8", + "re2": "1.20.9", "redis-lock": "0.1.4", - "reflect-metadata": "0.1.13", + "reflect-metadata": "0.1.14", "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.1", @@ -158,19 +163,18 @@ "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", - "systeminformation": "5.21.17", + "systeminformation": "5.21.20", "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.8", "tsconfig-paths": "4.2.0", - "twemoji-parser": "14.0.0", "typeorm": "0.3.17", - "typescript": "5.2.2", + "typescript": "5.3.3", "ulid": "2.3.0", "uuid": "^9.0.1", "vary": "1.1.2", "web-push": "3.6.6", - "ws": "8.14.2", + "ws": "8.15.1", "xev": "3.0.2" }, "devDependencies": { @@ -178,7 +182,7 @@ "@simplewebauthn/typescript-types": "8.3.4", "@swc/jest": "0.2.29", "@types/accepts": "1.3.7", - "@types/archiver": "6.0.1", + "@types/archiver": "6.0.2", "@types/bcryptjs": "2.4.6", "@types/body-parser": "1.19.5", "@types/cbor": "6.0.0", @@ -186,46 +190,47 @@ "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.24", "@types/http-link-header": "1.0.5", - "@types/jest": "29.5.8", + "@types/jest": "29.5.11", "@types/js-yaml": "4.0.9", - "@types/jsdom": "21.1.5", - "@types/jsonld": "1.5.12", + "@types/jsdom": "21.1.6", + "@types/jsonld": "1.5.13", "@types/jsrsasign": "10.5.12", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.9.1", + "@types/node": "20.10.5", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.14", "@types/oauth": "0.9.4", "@types/oauth2orize": "1.11.3", "@types/oauth2orize-pkce": "0.1.2", "@types/pg": "8.10.9", - "@types/pug": "2.0.9", - "@types/punycode": "2.1.2", + "@types/pug": "2.0.10", + "@types/punycode": "2.1.3", "@types/qrcode": "1.5.5", "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", "@types/rename": "1.0.7", - "@types/sanitize-html": "2.9.4", - "@types/semver": "7.5.5", + "@types/sanitize-html": "2.9.5", + "@types/semver": "7.5.6", "@types/sharp": "0.32.0", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", "@types/tinycolor2": "1.4.6", "@types/tmp": "0.2.6", + "@types/uuid": "^9.0.4", "@types/vary": "1.1.3", "@types/web-push": "3.6.3", - "@types/ws": "8.5.9", - "@types/uuid": "^9.0.4", - "@typescript-eslint/eslint-plugin": "6.11.0", - "@typescript-eslint/parser": "6.11.0", + "@types/ws": "8.5.10", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", "aws-sdk-client-mock": "3.0.0", "cross-env": "7.0.3", - "eslint": "8.53.0", - "eslint-plugin-import": "2.29.0", + "eslint": "8.56.0", + "eslint-plugin-import": "2.29.1", "execa": "8.0.1", "jest": "29.7.0", "jest-mock": "29.7.0", + "nodemon": "3.0.2", "simple-oauth2": "5.0.0" } } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index b25554b229..a550fdc364 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -7,6 +7,7 @@ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import * as yaml from 'js-yaml'; +import { globSync } from 'glob'; import type { RedisOptions } from 'ioredis'; type RedisOptionsSource = Partial & { @@ -64,6 +65,7 @@ type Source = { allowedPrivateNetworks?: string[]; maxFileSize?: number; + maxNoteLength?: number; clusterLimit?: number; @@ -85,7 +87,10 @@ type Source = { proxyRemoteFiles?: boolean; videoThumbnailGenerator?: string; + customMOTD?: string[]; + signToActivityPubGet?: boolean; + checkActivityPubGetSignature?: boolean; perChannelMaxNoteCacheCount?: number; perUserNotificationsMaxCount?: number; @@ -129,6 +134,7 @@ export type Config = { proxyBypassHosts: string[] | undefined; allowedPrivateNetworks: string[] | undefined; maxFileSize: number | undefined; + maxNoteLength: number; clusterLimit: number | undefined; id: string; outgoingAddress: string | undefined; @@ -142,7 +148,9 @@ export type Config = { deliverJobMaxAttempts: number | undefined; inboxJobMaxAttempts: number | undefined; proxyRemoteFiles: boolean | undefined; + customMOTD: string[] | undefined; signToActivityPubGet: boolean | undefined; + checkActivityPubGetSignature: boolean | undefined; version: string; host: string; @@ -188,11 +196,18 @@ const path = process.env.MISSKEY_CONFIG_YML export function loadConfig(): Config { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); - const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); + const clientManifestExists = fs.existsSync(`${_dirname}/../../../built/_vite_/manifest.json`); const clientManifest = clientManifestExists ? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; - const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; + + const config = globSync(path).sort() + .map(path => fs.readFileSync(path, 'utf-8')) + .map(contents => yaml.load(contents) as Source) + .reduce( + (acc: Source, cur: Source) => Object.assign(acc, cur), + {} as Source, + ) as Source; const url = tryCreateUrl(config.url); const version = meta.version; @@ -236,6 +251,7 @@ export function loadConfig(): Config { proxyBypassHosts: config.proxyBypassHosts, allowedPrivateNetworks: config.allowedPrivateNetworks, maxFileSize: config.maxFileSize, + maxNoteLength: config.maxNoteLength ?? 3000, clusterLimit: config.clusterLimit, outgoingAddress: config.outgoingAddress, outgoingAddressFamily: config.outgoingAddressFamily, @@ -248,7 +264,9 @@ export function loadConfig(): Config { deliverJobMaxAttempts: config.deliverJobMaxAttempts, inboxJobMaxAttempts: config.inboxJobMaxAttempts, proxyRemoteFiles: config.proxyRemoteFiles, + customMOTD: config.customMOTD, signToActivityPubGet: config.signToActivityPubGet, + checkActivityPubGetSignature: config.checkActivityPubGetSignature, mediaProxy: externalMediaProxy ?? internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, videoThumbnailGenerator: config.videoThumbnailGenerator ? diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 65be275548..2c27a02559 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -16,7 +16,7 @@ import type { AntennasRepository, UserListMembershipsRepository } from '@/models import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -39,7 +39,7 @@ export class AntennaService implements OnApplicationShutdown { private utilityService: UtilityService, private globalEventService: GlobalEventService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, ) { this.antennasFetched = false; this.antennas = []; @@ -60,11 +60,21 @@ export class AntennaService implements OnApplicationShutdown { lastUsedAt: new Date(body.lastUsedAt), }); break; - case 'antennaUpdated': - this.antennas[this.antennas.findIndex(a => a.id === body.id)] = { - ...body, - lastUsedAt: new Date(body.lastUsedAt), - }; + case 'antennaUpdated': { + const idx = this.antennas.findIndex(a => a.id === body.id); + if (idx >= 0) { + this.antennas[idx] = { + ...body, + lastUsedAt: new Date(body.lastUsedAt), + }; + } else { + // サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり + this.antennas.push({ + ...body, + lastUsedAt: new Date(body.lastUsedAt), + }); + } + } break; case 'antennaDeleted': this.antennas = this.antennas.filter(a => a.id !== body.id); @@ -84,7 +94,7 @@ export class AntennaService implements OnApplicationShutdown { const redisPipeline = this.redisForTimelines.pipeline(); for (const antenna of matchedAntennas) { - this.funoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline); + this.fanoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline); this.globalEventService.publishAntennaStream(antenna.id, 'note', note); } diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index b3ab901fed..fa868ff8b0 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -4,6 +4,7 @@ */ import { Module } from '@nestjs/common'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AnnouncementService } from './AnnouncementService.js'; @@ -62,7 +63,7 @@ import { FileInfoService } from './FileInfoService.js'; import { SearchService } from './SearchService.js'; import { ClipService } from './ClipService.js'; import { FeaturedService } from './FeaturedService.js'; -import { FunoutTimelineService } from './FunoutTimelineService.js'; +import { FanoutTimelineService } from './FanoutTimelineService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js'; import { RegistryApiService } from './RegistryApiService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js'; @@ -194,7 +195,8 @@ const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: Fi const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; -const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService }; +const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService }; +const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; @@ -330,7 +332,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting SearchService, ClipService, FeaturedService, - FunoutTimelineService, + FanoutTimelineService, + FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, ChartLoggerService, @@ -459,7 +462,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $SearchService, $ClipService, $FeaturedService, - $FunoutTimelineService, + $FanoutTimelineService, + $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, $ChartLoggerService, @@ -589,7 +593,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting SearchService, ClipService, FeaturedService, - FunoutTimelineService, + FanoutTimelineService, + FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, FederationChart, @@ -717,7 +722,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $SearchService, $ClipService, $FeaturedService, - $FunoutTimelineService, + $FanoutTimelineService, + $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, $FederationChart, diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index c9da3f77c0..7fc7800783 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -3,16 +3,19 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { URLSearchParams } from 'node:url'; import * as nodemailer from 'nodemailer'; import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; import { MetaService } from '@/core/MetaService.js'; +import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; @Injectable() export class EmailService { @@ -27,6 +30,8 @@ export class EmailService { private metaService: MetaService, private loggerService: LoggerService, + private utilityService: UtilityService, + private httpRequestService: HttpRequestService, ) { this.logger = this.loggerService.getLogger('email'); } @@ -151,7 +156,7 @@ export class EmailService { @bindThis public async validateEmailForAccount(emailAddress: string): Promise<{ available: boolean; - reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; + reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned'; }> { const meta = await this.metaService.fetch(); @@ -160,21 +165,38 @@ export class EmailService { email: emailAddress, }); - const validated = meta.enableActiveEmailValidation ? await validateEmail({ - email: emailAddress, - validateRegex: true, - validateMx: true, - validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので - validateDisposable: true, // 捨てアドかどうかチェック - validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので - }) : { valid: true, reason: null }; + let validated: { + valid: boolean, + reason?: string | null, + }; - const available = exist === 0 && validated.valid; + if (meta.enableActiveEmailValidation) { + if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) { + validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); + } else { + validated = await validateEmail({ + email: emailAddress, + validateRegex: true, + validateMx: true, + validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので + validateDisposable: true, // 捨てアドかどうかチェック + validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので + }); + } + } else { + validated = { valid: true, reason: null }; + } + + const emailDomain: string = emailAddress.split('@')[1]; + const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain); + + const available = exist === 0 && validated.valid && !isBanned; return { available, reason: available ? null : exist !== 0 ? 'used' : + isBanned ? 'banned' : validated.reason === 'regex' ? 'format' : validated.reason === 'disposable' ? 'disposable' : validated.reason === 'mx' ? 'mx' : @@ -182,4 +204,65 @@ export class EmailService { null, }; } + + private async verifyMail(emailAddress: string, verifymailAuthKey: string): Promise<{ + valid: boolean; + reason: 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | null; + }> { + const endpoint = 'https://verifymail.io/api/' + emailAddress + '?key=' + verifymailAuthKey; + const res = await this.httpRequestService.send(endpoint, { + method: 'GET', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json, */*', + }, + }); + + const json = (await res.json()) as { + block: boolean; + catch_all: boolean; + deliverable_email: boolean; + disposable: boolean; + domain: string; + email_address: string; + email_provider: string; + mx: boolean; + mx_fallback: boolean; + mx_host: string[]; + mx_ip: string[]; + mx_priority: { [key: string]: number }; + privacy: boolean; + related_domains: string[]; + }; + + if (json.email_address === undefined) { + return { + valid: false, + reason: 'format', + }; + } + if (json.deliverable_email !== undefined && !json.deliverable_email) { + return { + valid: false, + reason: 'smtp', + }; + } + if (json.disposable) { + return { + valid: false, + reason: 'disposable', + }; + } + if (json.mx !== undefined && !json.mx) { + return { + valid: false, + reason: 'mx', + }; + } + + return { + valid: true, + reason: null, + }; + } } diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts new file mode 100644 index 0000000000..6d857d189e --- /dev/null +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -0,0 +1,192 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; +import { Packed } from '@/misc/json-schema.js'; +import type { NotesRepository } from '@/models/_.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { isPureRenote } from '@/misc/is-pure-renote.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isReply } from '@/misc/is-reply.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; + +type TimelineOptions = { + untilId: string | null, + sinceId: string | null, + limit: number, + allowPartial: boolean, + me?: { id: MiUser['id'] } | undefined | null, + useDbFallback: boolean, + redisTimelines: FanoutTimelineName[], + noteFilter?: (note: MiNote) => boolean, + alwaysIncludeMyNotes?: boolean; + ignoreAuthorFromBlock?: boolean; + ignoreAuthorFromMute?: boolean; + excludeNoFiles?: boolean; + excludeReplies?: boolean; + excludeBots?: boolean; + excludePureRenotes: boolean; + dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, +}; + +@Injectable() +export class FanoutTimelineEndpointService { + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private cacheService: CacheService, + private fanoutTimelineService: FanoutTimelineService, + ) { + } + + @bindThis + async timeline(ps: TimelineOptions): Promise[]> { + return await this.noteEntityService.packMany(await this.getMiNotes(ps), ps.me); + } + + @bindThis + private async getMiNotes(ps: TimelineOptions): Promise { + let noteIds: string[]; + let shouldFallbackToDb = false; + + // 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える + if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); + + const shouldPrepend = ps.sinceId && !ps.untilId; + const idCompare: (a: string, b: string) => number = shouldPrepend ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1; + + const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId); + + // TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい + const redisResultIds = Array.from(new Set(redisResult.flat(1))); + + redisResultIds.sort(idCompare); + noteIds = redisResultIds.slice(0, ps.limit); + + shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); + + if (!shouldFallbackToDb) { + let filter = ps.noteFilter ?? (_note => true); + + if (ps.alwaysIncludeMyNotes && ps.me) { + const me = ps.me; + const parentFilter = filter; + filter = (note) => note.userId === me.id || parentFilter(note); + } + + if (ps.excludeNoFiles) { + const parentFilter = filter; + filter = (note) => note.fileIds.length !== 0 && parentFilter(note); + } + + if (ps.excludeReplies) { + const parentFilter = filter; + filter = (note) => !isReply(note, ps.me?.id) && parentFilter(note); + } + + if (ps.excludeBots) { + const parentFilter = filter; + filter = (note) => !note.user?.isBot && parentFilter(note); + } + + if (ps.excludePureRenotes) { + const parentFilter = filter; + filter = (note) => !isPureRenote(note) && parentFilter(note); + } + + if (ps.me) { + const me = ps.me; + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + userMutedInstances, + ] = await Promise.all([ + this.cacheService.userMutingsCache.fetch(ps.me.id), + this.cacheService.renoteMutingsCache.fetch(ps.me.id), + this.cacheService.userBlockedCache.fetch(ps.me.id), + this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)), + ]); + + const parentFilter = filter; + filter = (note) => { + if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false; + if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; + if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false; + if (isInstanceMuted(note, userMutedInstances)) return false; + + return parentFilter(note); + }; + } + + const redisTimeline: MiNote[] = []; + let readFromRedis = 0; + let lastSuccessfulRate = 1; // rateをキャッシュする? + + while ((redisResultIds.length - readFromRedis) !== 0) { + const remainingToRead = ps.limit - redisTimeline.length; + + // DBからの取り直しを減らす初回と同じ割合以上で成功すると仮定するが、クエリの長さを考えて三倍まで + const countToGet = Math.ceil(remainingToRead * Math.min(1.1 / lastSuccessfulRate, 3)); + noteIds = redisResultIds.slice(readFromRedis, readFromRedis + countToGet); + + readFromRedis += noteIds.length; + + const gotFromDb = await this.getAndFilterFromDb(noteIds, filter, idCompare); + redisTimeline.push(...gotFromDb); + lastSuccessfulRate = gotFromDb.length / noteIds.length; + + if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) { + // 十分Redisからとれた + const result = redisTimeline.slice(0, ps.limit); + if (shouldPrepend) result.reverse(); + return result; + } + } + + // まだ足りない分はDBにフォールバック + const remainingToRead = ps.limit - redisTimeline.length; + let dbUntil: string | null; + let dbSince: string | null; + if (shouldPrepend) { + redisTimeline.reverse(); + dbUntil = ps.untilId; + dbSince = noteIds[noteIds.length - 1]; + } else { + dbUntil = noteIds[noteIds.length - 1]; + dbSince = ps.sinceId; + } + const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead); + return shouldPrepend ? [...gotFromDb, ...redisTimeline] : [...redisTimeline, ...gotFromDb]; + } + + return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit); + } + + private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean, idCompare: (a: string, b: string) => number): Promise { + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + const notes = (await query.getMany()).filter(noteFilter); + + notes.sort((a, b) => idCompare(a.id, b.id)); + + return notes; + } +} diff --git a/packages/backend/src/core/FunoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts similarity index 64% rename from packages/backend/src/core/FunoutTimelineService.ts rename to packages/backend/src/core/FanoutTimelineService.ts index c633c329e5..9b2678fbcd 100644 --- a/packages/backend/src/core/FunoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -9,8 +9,37 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +export type FanoutTimelineName = + // home timeline + | `homeTimeline:${string}` + | `homeTimelineWithFiles:${string}` // only notes with files are included + // local timeline + | `localTimeline` // replies are not included + | `localTimelineWithFiles` // only non-reply notes with files are included + | `localTimelineWithReplies` // only replies are included + | `localTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id. + + // antenna + | `antennaTimeline:${string}` + + // user timeline + | `userTimeline:${string}` // replies are not included + | `userTimelineWithFiles:${string}` // only non-reply notes with files are included + | `userTimelineWithReplies:${string}` // only replies are included + | `userTimelineWithChannel:${string}` // only channel notes are included, replies are included + + // user list timelines + | `userListTimeline:${string}` + | `userListTimelineWithFiles:${string}` // only notes with files are included + + // channel timelines + | `channelTimeline:${string}` // replies are included + + // role timelines + | `roleTimeline:${string}` // any notes are included + @Injectable() -export class FunoutTimelineService { +export class FanoutTimelineService { constructor( @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, @@ -20,7 +49,7 @@ export class FunoutTimelineService { } @bindThis - public push(tl: string, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { + public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { // リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、 // 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) { @@ -41,7 +70,7 @@ export class FunoutTimelineService { } @bindThis - public get(name: string, untilId?: string | null, sinceId?: string | null) { + public get(name: FanoutTimelineName, untilId?: string | null, sinceId?: string | null) { if (untilId && sinceId) { return this.redisForTimelines.lrange('list:' + name, 0, -1) .then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1)); @@ -58,7 +87,7 @@ export class FunoutTimelineService { } @bindThis - public getMulti(name: string[], untilId?: string | null, sinceId?: string | null): Promise { + public getMulti(name: FanoutTimelineName[], untilId?: string | null, sinceId?: string | null): Promise { const pipeline = this.redisForTimelines.pipeline(); for (const n of name) { pipeline.lrange('list:' + n, 0, -1); @@ -79,7 +108,7 @@ export class FunoutTimelineService { } @bindThis - public purge(name: string) { + public purge(name: FanoutTimelineName) { return this.redisForTimelines.del('list:' + name); } } diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts index 9617f83880..595383c82c 100644 --- a/packages/backend/src/core/FeaturedService.ts +++ b/packages/backend/src/core/FeaturedService.ts @@ -5,14 +5,17 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { MiNote, MiUser } from '@/models/_.js'; +import type { MiGalleryPost, MiNote, MiUser } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと +export const GALLERY_POSTS_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと +const featuredEpoc = new Date('2023-01-01T00:00:00Z').getTime(); + @Injectable() export class FeaturedService { constructor( @@ -23,7 +26,7 @@ export class FeaturedService { @bindThis private getCurrentWindow(windowRange: number): number { - const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime(); + const passed = new Date().getTime() - featuredEpoc; return Math.floor(passed / windowRange); } @@ -74,11 +77,27 @@ export class FeaturedService { return Array.from(ranking.keys()); } + @bindThis + private async removeFromRanking(name: string, windowRange: number, element: string): Promise { + const currentWindow = this.getCurrentWindow(windowRange); + const previousWindow = currentWindow - 1; + + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.zrem(`${name}:${currentWindow}`, element); + redisPipeline.zrem(`${name}:${previousWindow}`, element); + await redisPipeline.exec(); + } + @bindThis public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise { return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score); } + @bindThis + public updateGalleryPostsRanking(galleryPostId: MiGalleryPost['id'], score = 1): Promise { + return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score); + } + @bindThis public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise { return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score); @@ -99,6 +118,11 @@ export class FeaturedService { return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold); } + @bindThis + public getGalleryPostsRanking(threshold: number): Promise { + return this.getRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, threshold); + } + @bindThis public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise { return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold); @@ -113,4 +137,9 @@ export class FeaturedService { public getHashtagsRanking(threshold: number): Promise { return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold); } + + @bindThis + public removeHashtagsFromRanking(hashtag: string): Promise { + return this.removeFromRanking('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag); + } } diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index d175f21f2f..95a4eba742 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -130,6 +130,9 @@ export interface NoteEventTypes { reaction: string; userId: MiUser['id']; }; + replied: { + id: MiNote['id']; + }; } type NoteStreamEventTypes = { [key in keyof NoteEventTypes]: { diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index d378999907..5a2417c9cd 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -15,6 +15,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { MetaService } from '@/core/MetaService.js'; +import { UtilityService } from '@/core/UtilityService.js'; @Injectable() export class HashtagService { @@ -29,6 +30,7 @@ export class HashtagService { private featuredService: FeaturedService, private idService: IdService, private metaService: MetaService, + private utilityService: UtilityService, ) { } @@ -161,6 +163,7 @@ export class HashtagService { const instance = await this.metaService.fetch(); const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); if (hiddenTags.includes(hashtag)) return; + if (this.utilityService.isSensitiveWordIncluded(hashtag, instance.sensitiveWords)) return; // YYYYMMDDHHmm (10分間隔) const now = new Date(); diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index c98b8ea6fc..43e72d2d7b 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -7,11 +7,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { ulid } from 'ulid'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { genAid, parseAid } from '@/misc/id/aid.js'; -import { genAidx, parseAidx } from '@/misc/id/aidx.js'; -import { genMeid, parseMeid } from '@/misc/id/meid.js'; -import { genMeidg, parseMeidg } from '@/misc/id/meidg.js'; -import { genObjectId, parseObjectId } from '@/misc/id/object-id.js'; +import { genAid, isSafeAidT, parseAid } from '@/misc/id/aid.js'; +import { genAidx, isSafeAidxT, parseAidx } from '@/misc/id/aidx.js'; +import { genMeid, isSafeMeidT, parseMeid } from '@/misc/id/meid.js'; +import { genMeidg, isSafeMeidgT, parseMeidg } from '@/misc/id/meidg.js'; +import { genObjectId, isSafeObjectIdT, parseObjectId } from '@/misc/id/object-id.js'; import { bindThis } from '@/decorators.js'; import { parseUlid } from '@/misc/id/ulid.js'; @@ -26,6 +26,19 @@ export class IdService { this.method = config.id.toLowerCase(); } + @bindThis + public isSafeT(t: number): boolean { + switch (this.method) { + case 'aid': return isSafeAidT(t); + case 'aidx': return isSafeAidxT(t); + case 'meid': return isSafeMeidT(t); + case 'meidg': return isSafeMeidgT(t); + case 'ulid': return t > 0; + case 'objectid': return isSafeObjectIdT(t); + default: throw new Error('unrecognized id generation method'); + } + } + /** * 時間を元にIDを生成します(省略時は現在日時) * @param time 日時 diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index 508544dc07..80e8020961 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -11,6 +11,7 @@ import { MiMeta } from '@/models/Meta.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -25,6 +26,7 @@ export class MetaService implements OnApplicationShutdown { @Inject(DI.db) private db: DataSource, + private featuredService: FeaturedService, private globalEventService: GlobalEventService, ) { //this.onMessage = this.onMessage.bind(this); @@ -95,6 +97,8 @@ export class MetaService implements OnApplicationShutdown { @bindThis public async update(data: Partial): Promise { + let before: MiMeta | undefined; + const updated = await this.db.transaction(async transactionalEntityManager => { const metas = await transactionalEntityManager.find(MiMeta, { order: { @@ -102,10 +106,10 @@ export class MetaService implements OnApplicationShutdown { }, }); - const meta = metas[0]; + before = metas[0]; - if (meta) { - await transactionalEntityManager.update(MiMeta, meta.id, data); + if (before) { + await transactionalEntityManager.update(MiMeta, before.id, data); const metas = await transactionalEntityManager.find(MiMeta, { order: { @@ -119,6 +123,21 @@ export class MetaService implements OnApplicationShutdown { } }); + if (data.hiddenTags) { + process.nextTick(() => { + const hiddenTags = new Set(data.hiddenTags); + if (before) { + for (const previousHiddenTag of before.hiddenTags) { + hiddenTags.delete(previousHiddenTag); + } + } + + for (const hiddenTag of hiddenTags) { + this.featuredService.removeHashtagsFromRanking(hiddenTag); + } + }); + } + this.globalEventService.publishInternalEvent('metaUpdated', updated); return updated; diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 178b4cc9a9..60bf8b3c2a 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -13,7 +13,7 @@ import { intersperse } from '@/misc/prelude/array.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; -import type * as mfm from 'mfm-js'; +import type * as mfm from '@sharkey/sfm-js'; const treeAdapter = TreeAdapter.defaultTreeAdapter; @@ -250,6 +250,12 @@ export class MfmService { } } + function fnDefault(node: mfm.MfmFn) { + const el = doc.createElement('i'); + appendChildren(node.children, el); + return el; + } + const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = { bold: (node) => { const el = doc.createElement('b'); @@ -276,17 +282,68 @@ export class MfmService { }, fn: (node) => { - if (node.props.name === 'unixtime') { - const text = node.children[0]!.type === 'text' ? node.children[0].props.text : ''; - const date = new Date(parseInt(text, 10) * 1000); - const el = doc.createElement('time'); - el.setAttribute('datetime', date.toISOString()); - el.textContent = date.toISOString(); - return el; - } else { - const el = doc.createElement('i'); - appendChildren(node.children, el); - return el; + switch (node.props.name) { + case 'unixtime': { + const text = node.children[0].type === 'text' ? node.children[0].props.text : ''; + try { + const date = new Date(parseInt(text, 10) * 1000); + const el = doc.createElement('time'); + el.setAttribute('datetime', date.toISOString()); + el.textContent = date.toISOString(); + return el; + } catch (err) { + return fnDefault(node); + } + } + + case 'ruby': { + if (node.children.length === 1) { + const child = node.children[0]; + const text = child.type === 'text' ? child.props.text : ''; + const rubyEl = doc.createElement('ruby'); + const rtEl = doc.createElement('rt'); + + // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする + const rpStartEl = doc.createElement('rp'); + rpStartEl.appendChild(doc.createTextNode('(')); + const rpEndEl = doc.createElement('rp'); + rpEndEl.appendChild(doc.createTextNode(')')); + + rubyEl.appendChild(doc.createTextNode(text.split(' ')[0])); + rtEl.appendChild(doc.createTextNode(text.split(' ')[1])); + rubyEl.appendChild(rpStartEl); + rubyEl.appendChild(rtEl); + rubyEl.appendChild(rpEndEl); + return rubyEl; + } else { + const rt = node.children.at(-1); + + if (!rt) { + return fnDefault(node); + } + + const text = rt.type === 'text' ? rt.props.text : ''; + const rubyEl = doc.createElement('ruby'); + const rtEl = doc.createElement('rt'); + + // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする + const rpStartEl = doc.createElement('rp'); + rpStartEl.appendChild(doc.createTextNode('(')); + const rpEndEl = doc.createElement('rp'); + rpEndEl.appendChild(doc.createTextNode(')')); + + appendChildren(node.children.slice(0, node.children.length - 1), rubyEl); + rtEl.appendChild(doc.createTextNode(text.trim())); + rubyEl.appendChild(rpStartEl); + rubyEl.appendChild(rtEl); + rubyEl.appendChild(rpEndEl); + return rubyEl; + } + } + + default: { + return fnDefault(node); + } } }, diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 837a53cfac..3bc4a29b99 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -4,7 +4,7 @@ */ import { setImmediate } from 'node:timers/promises'; -import * as mfm from 'mfm-js'; +import * as mfm from '@sharkey/sfm-js'; import { In, DataSource, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; @@ -54,9 +54,10 @@ import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { isReply } from '@/misc/is-reply.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -194,7 +195,7 @@ export class NoteCreateService implements OnApplicationShutdown { private idService: IdService, private globalEventService: GlobalEventService, private queueService: QueueService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private noteReadService: NoteReadService, private notificationService: NotificationService, private relayService: RelayService, @@ -252,7 +253,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.visibility === 'public' && data.channel == null) { const sensitiveWords = meta.sensitiveWords; - if (this.isSensitive(data, sensitiveWords)) { + if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; @@ -368,6 +369,14 @@ export class NoteCreateService implements OnApplicationShutdown { } } + if (user.host && !data.cw) { + await this.federatedInstanceService.fetch(user.host).then(async i => { + if (i.isNSFW) { + data.cw = 'Instance is marked as NSFW'; + } + }); + } + const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); setImmediate('post created', { signal: this.#shutdownController.signal }).then( @@ -413,7 +422,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.visibility === 'public' && data.channel == null) { const sensitiveWords = meta.sensitiveWords; - if (this.isSensitive(data, sensitiveWords)) { + if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; @@ -453,7 +462,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // Check blocking - if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) { + if (data.renote && !this.isQuote(data)) { if (data.renote.userHost === null) { if (data.renote.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); @@ -771,6 +780,9 @@ export class NoteCreateService implements OnApplicationShutdown { // If has in reply to note if (data.reply) { + this.globalEventService.publishNoteStream(data.reply.id, 'replied', { + id: note.id, + }); // 通知 if (data.reply.userHost === null) { const isThreadMuted = await this.noteThreadMutingsRepository.exist({ @@ -796,7 +808,7 @@ export class NoteCreateService implements OnApplicationShutdown { // If it is renote if (data.renote) { - const type = data.text ? 'quote' : 'renote'; + const type = this.isQuote(data) ? 'quote' : 'renote'; // Notify if (data.renote.userHost === null) { @@ -978,28 +990,9 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private isSensitive(note: Option, sensitiveWord: string[]): boolean { - if (sensitiveWord.length > 0) { - const text = note.cw ?? note.text ?? ''; - if (text === '') return false; - const matched = sensitiveWord.some(filter => { - // represents RegExp - const regexp = filter.match(/^\/(.+)\/(.*)$/); - // This should never happen due to input sanitisation. - if (!regexp) { - const words = filter.split(' '); - return words.every(keyword => text.includes(keyword)); - } - try { - return new RE2(regexp[1], regexp[2]).test(text); - } catch (err) { - // This should never happen due to input sanitisation. - return false; - } - }); - if (matched) return true; - } - return false; + private isQuote(note: Option): note is Option & { renote: MiNote } { + // sync with misc/is-quote.ts + return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll); } @bindThis @@ -1067,7 +1060,7 @@ export class NoteCreateService implements OnApplicationShutdown { private async renderNoteOrRenoteActivity(data: Option, note: MiNote) { if (data.localOnly) return null; - const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0) + const content = data.renote && !this.isQuote(data) ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); @@ -1117,9 +1110,9 @@ export class NoteCreateService implements OnApplicationShutdown { const r = this.redisForTimelines.pipeline(); if (note.channelId) { - this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); + this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -1129,9 +1122,9 @@ export class NoteCreateService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -1165,13 +1158,13 @@ export class NoteCreateService implements OnApplicationShutdown { if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue; // 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === following.followerId)) { + if (isReply(note, following.followerId)) { if (!following.withReplies) continue; } - this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -1179,44 +1172,48 @@ export class NoteCreateService implements OnApplicationShutdown { // ダイレクトのとき、そのリストが対象外のユーザーの場合 if ( note.visibility === 'specified' && + note.userId !== userListMembership.userListUserId && !note.visibleUserIds.some(v => v === userListMembership.userListUserId) ) continue; // 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) { + if (isReply(note, userListMembership.userListUserId)) { if (!userListMembership.withReplies) continue; } - this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); } } if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL - this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } // 自分自身以外への返信 - if (note.replyId && note.replyUserId !== note.userId) { - this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + if (isReply(note)) { + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + if (note.replyUserHost == null) { + this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r); + } } } else { - this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimeline', note.id, 1000, r); + this.fanoutTimelineService.push('localTimeline', note.id, 1000, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); + this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } } diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index e5918e6c2f..dae3f485ff 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -4,7 +4,7 @@ */ import { setImmediate } from 'node:timers/promises'; -import * as mfm from 'mfm-js'; +import * as mfm from '@sharkey/sfm-js'; import { DataSource, In, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; @@ -44,7 +44,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -191,7 +191,7 @@ export class NoteEditService implements OnApplicationShutdown { private idService: IdService, private globalEventService: GlobalEventService, private queueService: QueueService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private noteReadService: NoteReadService, private notificationService: NotificationService, private relayService: RelayService, @@ -786,9 +786,9 @@ export class NoteEditService implements OnApplicationShutdown { const r = this.redisForTimelines.pipeline(); if (note.channelId) { - this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); + this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -798,9 +798,9 @@ export class NoteEditService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -838,9 +838,9 @@ export class NoteEditService implements OnApplicationShutdown { if (!following.withReplies) continue; } - this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -856,36 +856,36 @@ export class NoteEditService implements OnApplicationShutdown { if (!userListMembership.withReplies) continue; } - this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); } } if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL - this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } // 自分自身以外への返信 if (note.replyId && note.replyUserId !== note.userId) { - this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); } } else { - this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimeline', note.id, 1000, r); + this.fanoutTimelineService.push('localTimeline', note.id, 1000, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); + this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } } diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index 52abb4c2a1..74e53c5c46 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -77,7 +77,7 @@ export class NotePiningService { } as MiUserNotePining); // Deliver to remote followers - if (this.userEntityService.isLocalUser(user)) { + if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { this.deliverPinnedChange(user.id, note.id, true); } } @@ -105,7 +105,7 @@ export class NotePiningService { }); // Deliver to remote followers - if (this.userEntityService.isLocalUser(user)) { + if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { this.deliverPinnedChange(user.id, noteId, false); } } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 4233b8d4c3..0daee34ce5 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; +import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -81,6 +81,9 @@ export class ReactionService { @Inject(DI.noteReactionsRepository) private noteReactionsRepository: NoteReactionsRepository, + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, @@ -138,7 +141,7 @@ export class ReactionService { reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; // センシティブ - if ((note.reactionAcceptance === 'nonSensitiveOnly') && emoji.isSensitive) { + if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) { reaction = FALLBACK; } } else { @@ -244,10 +247,19 @@ export class ReactionService { // リアクションされたユーザーがローカルユーザーなら通知を作成 if (note.userHost === null) { - this.notificationService.createNotification(note.userId, 'reaction', { - noteId: note.id, - reaction: reaction, - }, user.id); + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ + where: { + userId: note.userId, + threadId: note.threadId ?? note.id, + }, + }); + + if (!isThreadMuted) { + this.notificationService.createNotification(note.userId, 'reaction', { + noteId: note.id, + reaction: reaction, + }, user.id); + } } //#region 配信 diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 4c5f883351..dcd9d7399f 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -6,7 +6,14 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { In } from 'typeorm'; -import type { MiRole, MiRoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js'; +import { ModuleRef } from '@nestjs/core'; +import type { + MiRole, + MiRoleAssignment, + RoleAssignmentsRepository, + RolesRepository, + UsersRepository, +} from '@/models/_.js'; import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; @@ -16,16 +23,18 @@ import { CacheService } from '@/core/CacheService.js'; import type { RoleCondFormulaValue } from '@/models/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { IdService } from '@/core/IdService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Packed } from '@/misc/json-schema.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; export type RolePolicies = { gtlAvailable: boolean; ltlAvailable: boolean; + btlAvailable: boolean; canPublicNote: boolean; canInvite: boolean; inviteLimit: number; @@ -48,11 +57,13 @@ export type RolePolicies = { userEachUserListsLimit: number; rateLimitFactor: number; canImportNotes: boolean; + avatarDecorationLimit: number; }; export const DEFAULT_POLICIES: RolePolicies = { gtlAvailable: true, ltlAvailable: true, + btlAvailable: false, canPublicNote: true, canInvite: false, inviteLimit: 0, @@ -75,20 +86,27 @@ export const DEFAULT_POLICIES: RolePolicies = { userEachUserListsLimit: 50, rateLimitFactor: 1, canImportNotes: true, + avatarDecorationLimit: 1, }; @Injectable() -export class RoleService implements OnApplicationShutdown { +export class RoleService implements OnApplicationShutdown, OnModuleInit { private rolesCache: MemorySingleCache; private roleAssignmentByUserIdCache: MemoryKVCache; + private notificationService: NotificationService; public static AlreadyAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {}; constructor( + private moduleRef: ModuleRef, + @Inject(DI.redis) private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @@ -107,7 +125,7 @@ export class RoleService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private idService: IdService, private moderationLogService: ModerationLogService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, ) { //this.onMessage = this.onMessage.bind(this); @@ -117,6 +135,10 @@ export class RoleService implements OnApplicationShutdown { this.redisForSub.on('message', this.onMessage); } + async onModuleInit() { + this.notificationService = this.moduleRef.get(NotificationService.name); + } + @bindThis private async onMessage(_: string, data: string): Promise { const obj = JSON.parse(data); @@ -303,6 +325,7 @@ export class RoleService implements OnApplicationShutdown { return { gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), + btlAvailable: calc('btlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), @@ -326,6 +349,7 @@ export class RoleService implements OnApplicationShutdown { userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), canImportNotes: calc('canImportNotes', vs => vs.some(v => v === true)), + avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), }; } @@ -424,6 +448,12 @@ export class RoleService implements OnApplicationShutdown { this.globalEventService.publishInternalEvent('userRoleAssigned', created); + if (role.isPublic) { + this.notificationService.createNotification(userId, 'roleAssigned', { + roleId: roleId, + }); + } + if (moderator) { const user = await this.usersRepository.findOneByOrFail({ id: userId }); this.moderationLogService.log(moderator, 'assignRole', { @@ -479,10 +509,10 @@ export class RoleService implements OnApplicationShutdown { public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise { const roles = await this.getUserRoles(note.userId); - const redisPipeline = this.redisClient.pipeline(); + const redisPipeline = this.redisForTimelines.pipeline(); for (const role of roles) { - this.funoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline); + this.fanoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline); this.globalEventService.publishRoleTimelineStream(role.id, 'note', note); } diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index b399ace0ae..57c7e4baba 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -12,6 +12,8 @@ import { MiNote } from '@/models/Note.js'; import { MiUser } from '@/models/_.js'; import type { NotesRepository } from '@/models/_.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { CacheService } from '@/core/CacheService.js'; import { QueryService } from '@/core/QueryService.js'; import { IdService } from '@/core/IdService.js'; import type { Index, MeiliSearch } from 'meilisearch'; @@ -74,6 +76,7 @@ export class SearchService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + private cacheService: CacheService, private queryService: QueryService, private idService: IdService, ) { @@ -230,8 +233,19 @@ export class SearchService { limit: pagination.limit, }); if (res.hits.length === 0) return []; - const notes = await this.notesRepository.findBy({ + const [ + userIdsWhoMeMuting, + userIdsWhoBlockingMe, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]) : [new Set(), new Set()]; + const notes = (await this.notesRepository.findBy({ id: In(res.hits.map(x => x.id)), + })).filter(note => { + if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; + return true; }); return notes.sort((a, b) => a.id > b.id ? -1 : 1); } else { diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index bd7f298021..d600ffb9d9 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -29,7 +29,7 @@ import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -84,7 +84,7 @@ export class UserFollowingService implements OnModuleInit { private webhookService: WebhookService, private apRendererService: ApRendererService, private accountMoveService: AccountMoveService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -304,8 +304,6 @@ export class UserFollowingService implements OnModuleInit { }); } }); - - this.funoutTimelineService.purge(`homeTimeline:${follower.id}`); } // Publish followed event @@ -373,8 +371,6 @@ export class UserFollowingService implements OnModuleInit { }); } }); - - this.funoutTimelineService.purge(`homeTimeline:${follower.id}`); } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 702c731fc3..b6e4e1e884 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -3,30 +3,34 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; import * as Redis from 'ioredis'; +import { ModuleRef } from '@nestjs/core'; import type { UserListMembershipsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import type { MiUserList } from '@/models/UserList.js'; import type { MiUserListMembership } from '@/models/UserListMembership.js'; import { IdService } from '@/core/IdService.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { bindThis } from '@/decorators.js'; -import { RoleService } from '@/core/RoleService.js'; import { QueueService } from '@/core/QueueService.js'; import { RedisKVCache } from '@/misc/cache.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { RoleService } from '@/core/RoleService.js'; @Injectable() -export class UserListService implements OnApplicationShutdown { +export class UserListService implements OnApplicationShutdown, OnModuleInit { public static TooManyUsersError = class extends Error {}; public membersCache: RedisKVCache>; + private roleService: RoleService; constructor( + private moduleRef: ModuleRef, + @Inject(DI.redis) private redisClient: Redis.Redis, @@ -38,7 +42,6 @@ export class UserListService implements OnApplicationShutdown { private userEntityService: UserEntityService, private idService: IdService, - private roleService: RoleService, private globalEventService: GlobalEventService, private proxyAccountService: ProxyAccountService, private queueService: QueueService, @@ -54,6 +57,10 @@ export class UserListService implements OnApplicationShutdown { this.redisForSub.on('message', this.onMessage); } + async onModuleInit() { + this.roleService = this.moduleRef.get(RoleService.name); + } + @bindThis private async onMessage(_: string, data: string): Promise { const obj = JSON.parse(data); diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index b95e41167b..5dec36c89e 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -6,6 +6,7 @@ import { URL } from 'node:url'; import { toASCII } from 'punycode'; import { Inject, Injectable } from '@nestjs/common'; +import RE2 from 're2'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; @@ -41,6 +42,33 @@ export class UtilityService { return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); } + @bindThis + public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean { + if (sensitiveWords.length === 0) return false; + if (text === '') return false; + + const regexpregexp = /^\/(.+)\/(.*)$/; + + const matched = sensitiveWords.some(filter => { + // represents RegExp + const regexp = filter.match(regexpregexp); + // This should never happen due to input sanitisation. + if (!regexp) { + const words = filter.split(' '); + return words.every(keyword => text.includes(keyword)); + } + try { + // TODO: RE2インスタンスをキャッシュ + return new RE2(regexp[1], regexp[2]).test(text); + } catch (err) { + // This should never happen due to input sanitisation. + return false; + } + }); + + return matched; + } + @bindThis public extractDbHost(uri: string): string { const url = new URL(uri); diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 5a7b07e666..d8616d293d 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -306,9 +306,15 @@ export class ApInboxService { this.logger.info(`Creating the (Re)Note: ${uri}`); const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc); + const createdAt = activity.published ? new Date(activity.published) : null; + + if (createdAt && createdAt < this.idService.parse(renote.id).date) { + this.logger.warn('skip: malformed createdAt'); + return; + } await this.noteCreateService.create(actor, { - createdAt: activity.published ? new Date(activity.published) : null, + createdAt, renote, visibility: activityAudience.visibility, visibleUsers: activityAudience.visibleUsers, diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts index c45c27f863..c19eb310d2 100644 --- a/packages/backend/src/core/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/activitypub/ApMfmService.ts @@ -4,7 +4,7 @@ */ import { Injectable } from '@nestjs/common'; -import * as mfm from 'mfm-js'; +import * as mfm from '@sharkey/sfm-js'; import { MfmService } from '@/core/MfmService.js'; import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 7464593bb6..f4d39d2408 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -6,7 +6,7 @@ import { createPublicKey, randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; -import * as mfm from 'mfm-js'; +import * as mfm from '@sharkey/sfm-js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index a4cd533892..2eff7c64e0 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -15,6 +15,7 @@ import { DriveService } from '@/core/DriveService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; import type { IObject } from '../type.js'; @@ -31,6 +32,7 @@ export class ApImageService { private apResolverService: ApResolverService, private driveService: DriveService, private apLoggerService: ApLoggerService, + private federatedInstanceService: FederatedInstanceService, ) { this.logger = this.apLoggerService.logger; } @@ -68,6 +70,12 @@ export class ApImageService { // 2. or the image is not sensitive const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive); + await this.federatedInstanceService.fetch(actor.host).then(async i => { + if (i.isNSFW) { + image.sensitive = true; + } + }); + const file = await this.driveService.uploadFromUrl({ url: image.url, user: actor, diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 72488b1c37..2595783e04 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -97,6 +97,10 @@ export class ApNoteService { return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } + if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { + return new Error('invalid Note: published timestamp is malformed'); + } + return null; } diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 7d16a7a80e..515b356dee 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -48,6 +48,7 @@ export class InstanceEntityService { themeColor: instance.themeColor, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, + isNSFW: instance.isNSFW, }; } diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index f74594ff0c..704081ed00 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -15,8 +15,8 @@ import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { FilterUnionByProperty, notificationTypes } from '@/types.js'; +import { RoleEntityService } from './RoleEntityService.js'; import type { OnModuleInit } from '@nestjs/common'; -import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; @@ -27,7 +27,7 @@ const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 're export class NotificationEntityService implements OnModuleInit { private userEntityService: UserEntityService; private noteEntityService: NoteEntityService; - private customEmojiService: CustomEmojiService; + private roleEntityService: RoleEntityService; constructor( private moduleRef: ModuleRef, @@ -43,14 +43,13 @@ export class NotificationEntityService implements OnModuleInit { //private userEntityService: UserEntityService, //private noteEntityService: NoteEntityService, - //private customEmojiService: CustomEmojiService, ) { } onModuleInit() { this.userEntityService = this.moduleRef.get('UserEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); - this.customEmojiService = this.moduleRef.get('CustomEmojiService'); + this.roleEntityService = this.moduleRef.get('RoleEntityService'); } @bindThis @@ -81,6 +80,7 @@ export class NotificationEntityService implements OnModuleInit { detail: false, }) ) : undefined; + const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined; return await awaitAll({ id: notification.id, @@ -92,6 +92,9 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'reaction' ? { reaction: notification.reaction, } : {}), + ...(notification.type === 'roleAssigned' ? { + role: role, + } : {}), ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), @@ -198,12 +201,14 @@ export class NotificationEntityService implements OnModuleInit { }); } else if (notification.type === 'renote:grouped') { const users = await Promise.all(notification.userIds.map(userId => { - const user = hint?.packedUsers != null - ? hint.packedUsers.get(userId) - : this.userEntityService.pack(userId!, { id: meId }, { - detail: false, - }); - return user; + const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null; + if (packedUser) { + return packedUser; + } + + return this.userEntityService.pack(userId, { id: meId }, { + detail: false, + }); })); return await awaitAll({ id: notification.id, @@ -214,6 +219,8 @@ export class NotificationEntityService implements OnModuleInit { }); } + const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined; + return await awaitAll({ id: notification.id, createdAt: new Date(notification.createdAt).toISOString(), @@ -224,6 +231,9 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'reaction' ? { reaction: notification.reaction, } : {}), + ...(notification.type === 'roleAssigned' ? { + role: role, + } : {}), ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index b9d5f27d54..adb7dfbf86 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -361,13 +361,13 @@ export class UserEntityService implements OnModuleInit { const mastoapi = !opts.detail ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null; const followingCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followingCount : - (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : + (profile.followingVisibility === 'public') || isMe ? user.followingCount : + (profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : null; const followersCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followersCount : - (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : + (profile.followersVisibility === 'public') || isMe ? user.followersCount : + (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; @@ -395,6 +395,8 @@ export class UserEntityService implements OnModuleInit { id: ud.id, angle: ud.angle || undefined, flipH: ud.flipH || undefined, + offsetX: ud.offsetX || undefined, + offsetY: ud.offsetY || undefined, url: decorations.find(d => d.id === ud.id)!.url, }))) : [], isBot: user.isBot, @@ -452,7 +454,8 @@ export class UserEntityService implements OnModuleInit { pinnedPageId: profile!.pinnedPageId, pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null, publicReactions: profile!.publicReactions, - ffVisibility: profile!.ffVisibility, + followersVisibility: profile!.followersVisibility, + followingVisibility: profile!.followingVisibility, twoFactorEnabled: profile!.twoFactorEnabled, usePasswordLessLogin: profile!.usePasswordLessLogin, securityKeys: profile!.twoFactorEnabled @@ -511,6 +514,7 @@ export class UserEntityService implements OnModuleInit { hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), unreadNotificationsCount: notificationsInfo?.unreadCount, mutedWords: profile!.mutedWords, + hardMutedWords: profile!.hardMutedWords, mutedInstances: profile!.mutedInstances, mutingNotificationTypes: [], // 後方互換性のため notificationRecieveConfig: profile!.notificationRecieveConfig, diff --git a/packages/backend/src/misc/api-permissions.ts b/packages/backend/src/misc/api-permissions.ts deleted file mode 100644 index 57c9308844..0000000000 --- a/packages/backend/src/misc/api-permissions.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const kinds = [ - 'read:account', - 'write:account', - 'read:blocks', - 'write:blocks', - 'read:drive', - 'write:drive', - 'read:favorites', - 'write:favorites', - 'read:following', - 'write:following', - 'read:messaging', - 'write:messaging', - 'read:mutes', - 'write:mutes', - 'write:notes', - 'read:notifications', - 'write:notifications', - 'read:reactions', - 'write:reactions', - 'write:votes', - 'read:pages', - 'write:pages', - 'write:page-likes', - 'read:page-likes', - 'read:user-groups', - 'write:user-groups', - 'read:channels', - 'write:channels', - 'read:gallery', - 'write:gallery', - 'read:gallery-likes', - 'write:gallery-likes', -]; -// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions). diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts index 2dd079bd7c..04c2f2e913 100644 --- a/packages/backend/src/misc/emoji-regex.ts +++ b/packages/backend/src/misc/emoji-regex.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -// taken from twemoji-parser/dist/lib/regex.js -const twemojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd\udec3-\udec5\udef0-\udef6]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedd-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7c\ude80-\ude86\ude90-\udeac\udeb0-\udeba\udec0-\udec2\uded0-\uded9\udee0-\udee7]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; +// taken from @twemoji/parser/dist/lib/regex.js +const twemojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b|\ud83d\udc26\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|\ud83e\udef0|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd\udec3-\udec5\udef1-\udef8]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedc-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude7c\ude80-\ude88\ude90-\udebd\udebf-\udec2\udece-\udedb\udee0-\udee8]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); export const emojiRegexAtStartToEnd = new RegExp(`^(${twemojiRegex.source})$`); diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts index 0b898d47e8..0e8dfd21f8 100644 --- a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts +++ b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as mfm from 'mfm-js'; +import * as mfm from '@sharkey/sfm-js'; import { unique } from '@/misc/prelude/array.js'; export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { diff --git a/packages/backend/src/misc/extract-hashtags.ts b/packages/backend/src/misc/extract-hashtags.ts index 3bd56e98eb..3598d90093 100644 --- a/packages/backend/src/misc/extract-hashtags.ts +++ b/packages/backend/src/misc/extract-hashtags.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as mfm from 'mfm-js'; +import * as mfm from '@sharkey/sfm-js'; import { unique } from '@/misc/prelude/array.js'; export function extractHashtags(nodes: mfm.MfmNode[]): string[] { diff --git a/packages/backend/src/misc/extract-mentions.ts b/packages/backend/src/misc/extract-mentions.ts index 272eb92192..b0897b05a8 100644 --- a/packages/backend/src/misc/extract-mentions.ts +++ b/packages/backend/src/misc/extract-mentions.ts @@ -5,7 +5,7 @@ // test is located in test/extract-mentions -import * as mfm from 'mfm-js'; +import * as mfm from '@sharkey/sfm-js'; export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { // TODO: 重複を削除 diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index e7b59f262b..de03f6793f 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -34,3 +34,7 @@ export function parseAid(id: string): { date: Date; } { const time = parseInt(id.slice(0, 8), 36) + TIME2000; return { date: new Date(time) }; } + +export function isSafeAidT(t: number): boolean { + return t > TIME2000; +} diff --git a/packages/backend/src/misc/id/aidx.ts b/packages/backend/src/misc/id/aidx.ts index bed223225a..9f457f6f0a 100644 --- a/packages/backend/src/misc/id/aidx.ts +++ b/packages/backend/src/misc/id/aidx.ts @@ -41,3 +41,7 @@ export function parseAidx(id: string): { date: Date; } { const time = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000; return { date: new Date(time) }; } + +export function isSafeAidxT(t: number): boolean { + return t > TIME2000; +} diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts index 366738de05..7646282edb 100644 --- a/packages/backend/src/misc/id/meid.ts +++ b/packages/backend/src/misc/id/meid.ts @@ -38,3 +38,7 @@ export function parseMeid(id: string): { date: Date; } { date: new Date(parseInt(id.slice(0, 12), 16) - 0x800000000000), }; } + +export function isSafeMeidT(t: number): boolean { + return t > 0; +} diff --git a/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts index 426a46970b..f2a55443ef 100644 --- a/packages/backend/src/misc/id/meidg.ts +++ b/packages/backend/src/misc/id/meidg.ts @@ -38,3 +38,7 @@ export function parseMeidg(id: string): { date: Date; } { date: new Date(parseInt(id.slice(1, 12), 16)), }; } + +export function isSafeMeidgT(t: number): boolean { + return t > 0; +} diff --git a/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts index 49bd9591c0..f5c3619fdb 100644 --- a/packages/backend/src/misc/id/object-id.ts +++ b/packages/backend/src/misc/id/object-id.ts @@ -38,3 +38,7 @@ export function parseObjectId(id: string): { date: Date; } { date: new Date(parseInt(id.slice(0, 8), 16) * 1000), }; } + +export function isSafeObjectIdT(t: number): boolean { + return t > 0; +} diff --git a/packages/backend/src/misc/is-instance-muted.ts b/packages/backend/src/misc/is-instance-muted.ts index b231058a95..35fe11849d 100644 --- a/packages/backend/src/misc/is-instance-muted.ts +++ b/packages/backend/src/misc/is-instance-muted.ts @@ -3,12 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { MiNote } from '@/models/Note.js'; import type { Packed } from './json-schema.js'; -export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set): boolean { - if (mutedInstances.has(note.user.host ?? '')) return true; - if (mutedInstances.has(note.reply?.user.host ?? '')) return true; - if (mutedInstances.has(note.renote?.user.host ?? '')) return true; +export function isInstanceMuted(note: Packed<'Note'> | MiNote, mutedInstances: Set): boolean { + if (mutedInstances.has(note.user?.host ?? '')) return true; + if (mutedInstances.has(note.reply?.user?.host ?? '')) return true; + if (mutedInstances.has(note.renote?.user?.host ?? '')) return true; return false; } diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts index 059f6a4b5f..db72d1d57a 100644 --- a/packages/backend/src/misc/is-quote.ts +++ b/packages/backend/src/misc/is-quote.ts @@ -7,5 +7,6 @@ import type { MiNote } from '@/models/Note.js'; // eslint-disable-next-line import/no-default-export export default function(note: MiNote): boolean { + // sync with NoteCreateService.isQuote return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); } diff --git a/packages/backend/src/misc/is-reply.ts b/packages/backend/src/misc/is-reply.ts new file mode 100644 index 0000000000..964c2aa153 --- /dev/null +++ b/packages/backend/src/misc/is-reply.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MiUser } from '@/models/User.js'; + +export function isReply(note: any, viewerId?: MiUser['id'] | undefined | null): boolean { + return note.replyId && note.replyUserId !== note.userId && note.replyUserId !== viewerId; +} diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 80c1041c62..176978d35f 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -36,6 +36,9 @@ import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; +import { packedSigninSchema } from '@/models/json-schema/signin.js'; +import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; +import { packedAdSchema } from '@/models/json-schema/ad.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -47,6 +50,7 @@ export const refs = { User: packedUserSchema, UserList: packedUserListSchema, + Ad: packedAdSchema, Announcement: packedAnnouncementSchema, App: packedAppSchema, Note: packedNoteSchema, @@ -71,6 +75,9 @@ export const refs = { EmojiSimple: packedEmojiSimpleSchema, EmojiDetailed: packedEmojiDetailedSchema, Flash: packedFlashSchema, + Signin: packedSigninSchema, + RoleLite: packedRoleLiteSchema, + Role: packedRoleSchema, }; export type Packed = SchemaType; diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts index b225d918d6..4200b1b461 100644 --- a/packages/backend/src/models/Instance.ts +++ b/packages/backend/src/models/Instance.ts @@ -144,4 +144,9 @@ export class MiInstance { nullable: true, }) public infoUpdatedAt: Date | null; + + @Column('boolean', { + default: false, + }) + public isNSFW: boolean; } diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 97bec444d6..4bf856e619 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -456,6 +456,17 @@ export class MiMeta { }) public enableActiveEmailValidation: boolean; + @Column('boolean', { + default: false, + }) + public enableVerifymailApi: boolean; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public verifymailAuthKey: string | null; + @Column('boolean', { default: true, }) @@ -499,6 +510,13 @@ export class MiMeta { }) public manifestJsonOverride: string; + @Column('varchar', { + length: 1024, + array: true, + default: '{}', + }) + public bannedEmailDomains: string[]; + @Column('varchar', { length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }', }) @@ -544,4 +562,9 @@ export class MiMeta { nullable: true, }) public defaultLike: string | null; + + @Column('varchar', { + length: 256, array: true, default: '{}', + }) + public bubbleInstances: string[]; } diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 1d5fc124e2..3bc2edaa0d 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -3,11 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { notificationTypes } from '@/types.js'; import { MiUser } from './User.js'; import { MiNote } from './Note.js'; -import { MiFollowRequest } from './FollowRequest.js'; import { MiAccessToken } from './AccessToken.js'; +import { MiRole } from './Role.js'; export type MiNotification = { type: 'note'; @@ -68,6 +67,11 @@ export type MiNotification = { id: string; createdAt: string; notifierId: MiUser['id']; +} | { + type: 'roleAssigned'; + id: string; + createdAt: string; + roleId: MiRole['id']; } | { type: 'achievementEarned'; id: string; diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 1e1c07bfc4..3db8b398fd 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -166,8 +166,10 @@ export class MiUser { }) public avatarDecorations: { id: string; - angle: number; - flipH: boolean; + angle?: number; + flipH?: boolean; + offsetX?: number; + offsetY?: number; }[]; @Index() diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 8520a09f0b..ae46fbc83c 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -4,7 +4,7 @@ */ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js'; +import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiPage } from './Page.js'; @@ -29,6 +29,7 @@ export class MiUserProfile { }) public location: string | null; + @Index() @Column('char', { length: 10, nullable: true, comment: 'The birthday (YYYY-MM-DD) of the User.', @@ -100,10 +101,16 @@ export class MiUserProfile { public publicReactions: boolean; @Column('enum', { - enum: ffVisibility, + enum: followingVisibilities, default: 'public', }) - public ffVisibility: typeof ffVisibility[number]; + public followingVisibility: typeof followingVisibilities[number]; + + @Column('enum', { + enum: followersVisibilities, + default: 'public', + }) + public followersVisibility: typeof followersVisibilities[number]; @Column('varchar', { length: 128, nullable: true, @@ -222,7 +229,12 @@ export class MiUserProfile { @Column('jsonb', { default: [], }) - public mutedWords: string[][]; + public mutedWords: (string[] | string)[]; + + @Column('jsonb', { + default: [], + }) + public hardMutedWords: (string[] | string)[]; @Column('jsonb', { default: [], diff --git a/packages/backend/src/models/json-schema/ad.ts b/packages/backend/src/models/json-schema/ad.ts new file mode 100644 index 0000000000..649ffcd4dc --- /dev/null +++ b/packages/backend/src/models/json-schema/ad.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedAdSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, + nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + expiresAt: { + type: 'string', + optional: false, + nullable: false, + format: 'date-time', + }, + startsAt: { + type: 'string', + optional: false, + nullable: false, + format: 'date-time', + }, + place: { + type: 'string', + optional: false, + nullable: false, + }, + priority: { + type: 'string', + optional: false, + nullable: false, + }, + ratio: { + type: 'number', + optional: false, + nullable: false, + }, + url: { + type: 'string', + optional: false, + nullable: false, + }, + imageUrl: { + type: 'string', + optional: false, + nullable: false, + }, + memo: { + type: 'string', + optional: false, + nullable: false, + }, + dayOfWeek: { + type: 'integer', + optional: false, + nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/announcement.ts b/packages/backend/src/models/json-schema/announcement.ts index c7e24c7f29..78a98872b2 100644 --- a/packages/backend/src/models/json-schema/announcement.ts +++ b/packages/backend/src/models/json-schema/announcement.ts @@ -42,11 +42,15 @@ export const packedAnnouncementSchema = { type: 'string', optional: false, nullable: false, }, - forYou: { + needConfirmationToRead: { type: 'boolean', optional: false, nullable: false, }, - needConfirmationToRead: { + silence: { + type: 'boolean', + optional: false, nullable: false, + }, + forYou: { type: 'boolean', optional: false, nullable: false, }, diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts index 8f9770cdc5..5b0fa0f15d 100644 --- a/packages/backend/src/models/json-schema/channel.ts +++ b/packages/backend/src/models/json-schema/channel.ts @@ -19,7 +19,7 @@ export const packedChannelSchema = { }, lastNotedAt: { type: 'string', - optional: false, nullable: true, + nullable: true, optional: false, format: 'date-time', }, name: { @@ -28,38 +28,18 @@ export const packedChannelSchema = { }, description: { type: 'string', - nullable: true, optional: false, - }, - bannerUrl: { - type: 'string', - format: 'url', - nullable: true, optional: false, - }, - isArchived: { - type: 'boolean', - optional: false, nullable: false, - }, - notesCount: { - type: 'number', - nullable: false, optional: false, - }, - usersCount: { - type: 'number', - nullable: false, optional: false, - }, - isFollowing: { - type: 'boolean', - optional: true, nullable: false, - }, - isFavorited: { - type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: true, }, userId: { type: 'string', nullable: true, optional: false, format: 'id', }, + bannerUrl: { + type: 'string', + format: 'url', + nullable: true, optional: false, + }, pinnedNoteIds: { type: 'array', nullable: false, optional: false, @@ -72,6 +52,18 @@ export const packedChannelSchema = { type: 'string', optional: false, nullable: false, }, + isArchived: { + type: 'boolean', + optional: false, nullable: false, + }, + usersCount: { + type: 'number', + nullable: false, optional: false, + }, + notesCount: { + type: 'number', + nullable: false, optional: false, + }, isSensitive: { type: 'boolean', optional: false, nullable: false, @@ -80,5 +72,22 @@ export const packedChannelSchema = { type: 'boolean', optional: false, nullable: false, }, + isFollowing: { + type: 'boolean', + optional: true, nullable: false, + }, + isFavorited: { + type: 'boolean', + optional: true, nullable: false, + }, + pinnedNotes: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/clip.ts b/packages/backend/src/models/json-schema/clip.ts index 64f7a2ad9c..1ab96c2b3b 100644 --- a/packages/backend/src/models/json-schema/clip.ts +++ b/packages/backend/src/models/json-schema/clip.ts @@ -44,13 +44,13 @@ export const packedClipSchema = { type: 'boolean', optional: false, nullable: false, }, - isFavorited: { - type: 'boolean', - optional: true, nullable: false, - }, favoritedCount: { type: 'number', optional: false, nullable: false, }, + isFavorited: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index 87f1340812..79f242a711 100644 --- a/packages/backend/src/models/json-schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts @@ -74,7 +74,7 @@ export const packedDriveFileSchema = { }, url: { type: 'string', - optional: false, nullable: true, + optional: false, nullable: false, format: 'url', }, thumbnailUrl: { diff --git a/packages/backend/src/models/json-schema/drive-folder.ts b/packages/backend/src/models/json-schema/drive-folder.ts index 51107d423f..aaad301303 100644 --- a/packages/backend/src/models/json-schema/drive-folder.ts +++ b/packages/backend/src/models/json-schema/drive-folder.ts @@ -21,6 +21,12 @@ export const packedDriveFolderSchema = { type: 'string', optional: false, nullable: false, }, + parentId: { + type: 'string', + optional: false, nullable: true, + format: 'id', + example: 'xxxxxxxxxx', + }, foldersCount: { type: 'number', optional: true, nullable: false, @@ -29,12 +35,6 @@ export const packedDriveFolderSchema = { type: 'number', optional: true, nullable: false, }, - parentId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - example: 'xxxxxxxxxx', - }, parent: { type: 'object', optional: true, nullable: true, diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 442e1076f2..94873716bf 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -79,6 +79,10 @@ export const packedFederationInstanceSchema = { type: 'string', optional: false, nullable: true, }, + isSilenced: { + type: 'boolean', + optional: false, nullable: false, + }, iconUrl: { type: 'string', optional: false, nullable: true, @@ -93,11 +97,6 @@ export const packedFederationInstanceSchema = { type: 'string', optional: false, nullable: true, }, - isSilenced: { - type: "boolean", - optional: false, - nullable: false, - }, infoUpdatedAt: { type: 'string', optional: false, nullable: true, @@ -108,5 +107,10 @@ export const packedFederationInstanceSchema = { optional: false, nullable: true, format: 'date-time', }, + isNSFW: { + type: 'boolean', + optional: false, + nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 9453ba1dce..f08fa7a279 100644 --- a/packages/backend/src/models/json-schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts @@ -22,6 +22,16 @@ export const packedFlashSchema = { optional: false, nullable: false, format: 'date-time', }, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, title: { type: 'string', optional: false, nullable: false, @@ -34,16 +44,6 @@ export const packedFlashSchema = { type: 'string', optional: false, nullable: false, }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, - }, likedCount: { type: 'number', optional: false, nullable: true, diff --git a/packages/backend/src/models/json-schema/following.ts b/packages/backend/src/models/json-schema/following.ts index 3a24ebb619..e92cff20a1 100644 --- a/packages/backend/src/models/json-schema/following.ts +++ b/packages/backend/src/models/json-schema/following.ts @@ -22,16 +22,16 @@ export const packedFollowingSchema = { optional: false, nullable: false, format: 'id', }, - followee: { - type: 'object', - optional: true, nullable: false, - ref: 'UserDetailed', - }, followerId: { type: 'string', optional: false, nullable: false, format: 'id', }, + followee: { + type: 'object', + optional: true, nullable: false, + ref: 'UserDetailed', + }, follower: { type: 'object', optional: true, nullable: false, diff --git a/packages/backend/src/models/json-schema/gallery-post.ts b/packages/backend/src/models/json-schema/gallery-post.ts index cf260c0bf5..df7038950c 100644 --- a/packages/backend/src/models/json-schema/gallery-post.ts +++ b/packages/backend/src/models/json-schema/gallery-post.ts @@ -22,14 +22,6 @@ export const packedGalleryPostSchema = { optional: false, nullable: false, format: 'date-time', }, - title: { - type: 'string', - optional: false, nullable: false, - }, - description: { - type: 'string', - optional: false, nullable: true, - }, userId: { type: 'string', optional: false, nullable: false, @@ -40,6 +32,14 @@ export const packedGalleryPostSchema = { ref: 'UserLite', optional: false, nullable: false, }, + title: { + type: 'string', + optional: false, nullable: false, + }, + description: { + type: 'string', + optional: false, nullable: true, + }, fileIds: { type: 'array', optional: true, nullable: false, @@ -70,5 +70,13 @@ export const packedGalleryPostSchema = { type: 'boolean', optional: false, nullable: false, }, + likedCount: { + type: 'number', + optional: false, nullable: false, + }, + isLiked: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 38c0054b55..aa749943f0 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -127,22 +127,26 @@ export const packedNoteSchema = { channel: { type: 'object', optional: true, nullable: true, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - }, - name: { - type: 'string', - optional: false, nullable: true, - }, - isSensitive: { - type: 'boolean', - optional: true, nullable: false, - }, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + color: { + type: 'string', + optional: false, nullable: false, + }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + allowRenoteToExternal: { + type: 'boolean', + optional: false, nullable: false, }, }, }, @@ -182,6 +186,10 @@ export const packedNoteSchema = { optional: false, nullable: false, }, }, + clippedCount: { + type: 'number', + optional: true, nullable: false, + }, myReaction: { type: 'object', diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 27db3bb62c..c6d6e84317 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -42,13 +42,9 @@ export const packedNotificationSchema = { type: 'string', optional: true, nullable: true, }, - choice: { - type: 'number', - optional: true, nullable: true, - }, - invitation: { - type: 'object', - optional: true, nullable: true, + achievement: { + type: 'string', + optional: true, nullable: false, }, body: { type: 'string', @@ -81,14 +77,14 @@ export const packedNotificationSchema = { required: ['user', 'reaction'], }, }, - }, - users: { - type: 'array', - optional: true, nullable: true, - items: { - type: 'object', - ref: 'UserLite', - optional: false, nullable: false, + users: { + type: 'array', + optional: true, nullable: true, + items: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, }, }, } as const; diff --git a/packages/backend/src/models/json-schema/page.ts b/packages/backend/src/models/json-schema/page.ts index 3f20a4b802..9baacd6884 100644 --- a/packages/backend/src/models/json-schema/page.ts +++ b/packages/backend/src/models/json-schema/page.ts @@ -22,6 +22,32 @@ export const packedPageSchema = { optional: false, nullable: false, format: 'date-time', }, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + content: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + }, + }, + variables: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + }, + }, title: { type: 'string', optional: false, nullable: false, @@ -34,23 +60,47 @@ export const packedPageSchema = { type: 'string', optional: false, nullable: true, }, - content: { - type: 'array', + hideTitleWhenPinned: { + type: 'boolean', optional: false, nullable: false, }, - variables: { - type: 'array', + alignCenter: { + type: 'boolean', optional: false, nullable: false, }, - userId: { + font: { type: 'string', optional: false, nullable: false, - format: 'id', }, - user: { - type: 'object', - ref: 'UserLite', + script: { + type: 'string', optional: false, nullable: false, }, + eyeCatchingImageId: { + type: 'string', + optional: false, nullable: true, + }, + eyeCatchingImage: { + type: 'object', + optional: false, nullable: true, + ref: 'DriveFile', + }, + attachedFiles: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'DriveFile', + }, + }, + likedCount: { + type: 'number', + optional: false, nullable: false, + }, + isLiked: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts new file mode 100644 index 0000000000..b0c6804bb8 --- /dev/null +++ b/packages/backend/src/models/json-schema/role.ts @@ -0,0 +1,158 @@ +const rolePolicyValue = { + type: 'object', + properties: { + value: { + oneOf: [ + { + type: 'integer', + optional: false, nullable: false, + }, + { + type: 'boolean', + optional: false, nullable: false, + }, + ], + }, + priority: { + type: 'integer', + optional: false, nullable: false, + }, + useDefault: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; + +export const packedRoleLiteSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + name: { + type: 'string', + optional: false, nullable: false, + example: 'New Role', + }, + color: { + type: 'string', + optional: false, nullable: true, + example: '#000000', + }, + iconUrl: { + type: 'string', + optional: false, nullable: true, + }, + description: { + type: 'string', + optional: false, nullable: false, + }, + isModerator: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + isAdministrator: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + displayOrder: { + type: 'integer', + optional: false, nullable: false, + example: 0, + }, + }, +} as const; + +export const packedRoleSchema = { + type: 'object', + allOf: [ + { + type: 'object', + ref: 'RoleLite', + }, + { + type: 'object', + properties: { + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + updatedAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + target: { + type: 'string', + optional: false, nullable: false, + enum: ['manual', 'conditional'], + }, + condFormula: { + type: 'object', + optional: false, nullable: false, + }, + isPublic: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + isExplorable: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + asBadge: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + canEditMembersByModerator: { + type: 'boolean', + optional: false, nullable: false, + example: false, + }, + policies: { + type: 'object', + optional: false, nullable: false, + properties: { + pinLimit: rolePolicyValue, + canInvite: rolePolicyValue, + clipLimit: rolePolicyValue, + canHideAds: rolePolicyValue, + inviteLimit: rolePolicyValue, + antennaLimit: rolePolicyValue, + gtlAvailable: rolePolicyValue, + ltlAvailable: rolePolicyValue, + webhookLimit: rolePolicyValue, + canPublicNote: rolePolicyValue, + userListLimit: rolePolicyValue, + wordMuteLimit: rolePolicyValue, + alwaysMarkNsfw: rolePolicyValue, + canSearchNotes: rolePolicyValue, + driveCapacityMb: rolePolicyValue, + rateLimitFactor: rolePolicyValue, + inviteLimitCycle: rolePolicyValue, + noteEachClipsLimit: rolePolicyValue, + inviteExpirationTime: rolePolicyValue, + canManageCustomEmojis: rolePolicyValue, + userEachUserListsLimit: rolePolicyValue, + canManageAvatarDecorations: rolePolicyValue, + canUseTranslator: rolePolicyValue, + avatarDecorationLimit: rolePolicyValue, + }, + }, + usersCount: { + type: 'integer', + optional: false, nullable: false, + }, + }, + }, + ], +} as const; diff --git a/packages/backend/src/models/json-schema/signin.ts b/packages/backend/src/models/json-schema/signin.ts new file mode 100644 index 0000000000..d27d2490c5 --- /dev/null +++ b/packages/backend/src/models/json-schema/signin.ts @@ -0,0 +1,26 @@ +export const packedSigninSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + ip: { + type: 'string', + optional: false, nullable: false, + }, + headers: { + type: 'object', + optional: false, nullable: false, + }, + success: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 5b12050541..af67e62afa 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -3,6 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +const notificationRecieveConfig = { + type: 'object', + nullable: false, optional: true, + properties: { + type: { + type: 'string', + nullable: false, optional: false, + enum: ['all', 'following', 'follower', 'mutualFollow', 'list', 'never'], + }, + }, +} as const; + export const packedUserLiteSchema = { type: 'object', properties: { @@ -49,11 +61,6 @@ export const packedUserLiteSchema = { nullable: false, optional: false, format: 'id', }, - url: { - type: 'string', - format: 'url', - nullable: false, optional: false, - }, angle: { type: 'number', nullable: false, optional: true, @@ -62,6 +69,19 @@ export const packedUserLiteSchema = { type: 'boolean', nullable: false, optional: true, }, + url: { + type: 'string', + format: 'url', + nullable: false, optional: false, + }, + offsetX: { + type: 'number', + nullable: false, optional: true, + }, + offsetY: { + type: 'number', + nullable: false, optional: true, + }, }, }, }, @@ -95,12 +115,67 @@ export const packedUserLiteSchema = { type: 'boolean', nullable: false, optional: true, }, + instance: { + type: 'object', + nullable: false, optional: true, + properties: { + name: { + type: 'string', + nullable: true, optional: false, + }, + softwareName: { + type: 'string', + nullable: true, optional: false, + }, + softwareVersion: { + type: 'string', + nullable: true, optional: false, + }, + iconUrl: { + type: 'string', + nullable: true, optional: false, + }, + faviconUrl: { + type: 'string', + nullable: true, optional: false, + }, + themeColor: { + type: 'string', + nullable: true, optional: false, + }, + }, + }, + emojis: { + type: 'object', + nullable: false, optional: false, + }, onlineStatus: { type: 'string', - format: 'url', - nullable: true, optional: false, + nullable: false, optional: false, enum: ['unknown', 'online', 'active', 'offline'], }, + badgeRoles: { + type: 'array', + nullable: false, optional: true, + items: { + type: 'object', + nullable: false, optional: false, + properties: { + name: { + type: 'string', + nullable: false, optional: false, + }, + iconUrl: { + type: 'string', + nullable: true, optional: false, + }, + displayOrder: { + type: 'number', + nullable: false, optional: false, + }, + }, + }, + }, }, } as const; @@ -117,21 +192,18 @@ export const packedUserDetailedNotMeOnlySchema = { format: 'uri', nullable: true, optional: false, }, - movedToUri: { + movedTo: { type: 'string', format: 'uri', - nullable: true, - optional: false, + nullable: true, optional: false, }, alsoKnownAs: { type: 'array', - nullable: true, - optional: false, + nullable: true, optional: false, items: { type: 'string', format: 'id', - nullable: false, - optional: false, + nullable: false, optional: false, }, }, createdAt: { @@ -191,10 +263,10 @@ export const packedUserDetailedNotMeOnlySchema = { example: '2018-03-12', }, ListenBrainz: { - type: "string", + type: 'string', nullable: true, optional: false, - example: "Steve", + example: 'Steve', }, lang: { type: 'string', @@ -272,6 +344,16 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + followingVisibility: { + type: 'string', + nullable: false, optional: false, + enum: ['public', 'followers', 'private'], + }, + followersVisibility: { + type: 'string', + nullable: false, optional: false, + enum: ['public', 'followers', 'private'], + }, twoFactorEnabled: { type: 'boolean', nullable: false, optional: false, @@ -287,6 +369,23 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: false, optional: false, default: false, }, + roles: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'object', + nullable: false, optional: false, + ref: 'RoleLite', + }, + }, + memo: { + type: 'string', + nullable: true, optional: false, + }, + moderationNote: { + type: 'string', + nullable: false, optional: true, + }, //#region relations isFollowing: { type: 'boolean', @@ -320,13 +419,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: true, }, - memo: { - type: 'string', - nullable: false, optional: true, - }, notify: { type: 'string', nullable: false, optional: true, + enum: ['normal', 'none'], }, withReplies: { type: 'boolean', @@ -354,29 +450,37 @@ export const packedMeDetailedOnlySchema = { nullable: true, optional: false, format: 'id', }, - injectFeaturedNote: { + isModerator: { type: 'boolean', nullable: true, optional: false, }, + isAdmin: { + type: 'boolean', + nullable: true, optional: false, + }, + injectFeaturedNote: { + type: 'boolean', + nullable: false, optional: false, + }, receiveAnnouncementEmail: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, alwaysMarkNsfw: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, autoSensitive: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, carefulBot: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, autoAcceptFollowed: { type: 'boolean', - nullable: true, optional: false, + nullable: false, optional: false, }, noCrawle: { type: 'boolean', @@ -415,10 +519,23 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + unreadAnnouncements: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'object', + nullable: false, optional: false, + ref: 'Announcement', + }, + }, hasUnreadAntenna: { type: 'boolean', nullable: false, optional: false, }, + hasUnreadChannel: { + type: 'boolean', + nullable: false, optional: false, + }, hasUnreadNotification: { type: 'boolean', nullable: false, optional: false, @@ -443,6 +560,18 @@ export const packedMeDetailedOnlySchema = { }, }, }, + hardMutedWords: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'string', + nullable: false, optional: false, + }, + }, + }, mutedInstances: { type: 'array', nullable: true, optional: false, @@ -454,15 +583,150 @@ export const packedMeDetailedOnlySchema = { notificationRecieveConfig: { type: 'object', nullable: false, optional: false, + properties: { + app: notificationRecieveConfig, + quote: notificationRecieveConfig, + reply: notificationRecieveConfig, + follow: notificationRecieveConfig, + renote: notificationRecieveConfig, + mention: notificationRecieveConfig, + reaction: notificationRecieveConfig, + pollEnded: notificationRecieveConfig, + receiveFollowRequest: notificationRecieveConfig, + }, }, emailNotificationTypes: { type: 'array', - nullable: true, optional: false, + nullable: false, optional: false, items: { type: 'string', nullable: false, optional: false, }, }, + achievements: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'object', + nullable: false, optional: false, + properties: { + name: { + type: 'string', + nullable: false, optional: false, + }, + unlockedAt: { + type: 'number', + nullable: false, optional: false, + }, + }, + }, + }, + loggedInDays: { + type: 'number', + nullable: false, optional: false, + }, + policies: { + type: 'object', + nullable: false, optional: false, + properties: { + gtlAvailable: { + type: 'boolean', + nullable: false, optional: false, + }, + ltlAvailable: { + type: 'boolean', + nullable: false, optional: false, + }, + canPublicNote: { + type: 'boolean', + nullable: false, optional: false, + }, + canInvite: { + type: 'boolean', + nullable: false, optional: false, + }, + inviteLimit: { + type: 'number', + nullable: false, optional: false, + }, + inviteLimitCycle: { + type: 'number', + nullable: false, optional: false, + }, + inviteExpirationTime: { + type: 'number', + nullable: false, optional: false, + }, + canManageCustomEmojis: { + type: 'boolean', + nullable: false, optional: false, + }, + canManageAvatarDecorations: { + type: 'boolean', + nullable: false, optional: false, + }, + canSearchNotes: { + type: 'boolean', + nullable: false, optional: false, + }, + canUseTranslator: { + type: 'boolean', + nullable: false, optional: false, + }, + canHideAds: { + type: 'boolean', + nullable: false, optional: false, + }, + driveCapacityMb: { + type: 'number', + nullable: false, optional: false, + }, + alwaysMarkNsfw: { + type: 'boolean', + nullable: false, optional: false, + }, + pinLimit: { + type: 'number', + nullable: false, optional: false, + }, + antennaLimit: { + type: 'number', + nullable: false, optional: false, + }, + wordMuteLimit: { + type: 'number', + nullable: false, optional: false, + }, + webhookLimit: { + type: 'number', + nullable: false, optional: false, + }, + clipLimit: { + type: 'number', + nullable: false, optional: false, + }, + noteEachClipsLimit: { + type: 'number', + nullable: false, optional: false, + }, + userListLimit: { + type: 'number', + nullable: false, optional: false, + }, + userEachUserListsLimit: { + type: 'number', + nullable: false, optional: false, + }, + rateLimitFactor: { + type: 'number', + nullable: false, optional: false, + }, + avatarDecorationLimit: { + type: 'number', + nullable: false, optional: false, + }, + }, + }, //#region secrets email: { type: 'string', @@ -478,6 +742,23 @@ export const packedMeDetailedOnlySchema = { items: { type: 'object', nullable: false, optional: false, + properties: { + id: { + type: 'string', + nullable: false, optional: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + name: { + type: 'string', + nullable: false, optional: false, + }, + lastUsed: { + type: 'string', + nullable: false, optional: false, + format: 'date-time', + }, + }, }, }, //#endregion @@ -539,5 +820,13 @@ export const packedUserSchema = { type: 'object', ref: 'UserDetailed', }, + { + type: 'object', + ref: 'UserDetailedNotMe', + }, + { + type: 'object', + ref: 'MeDetailed', + }, ], } as const; diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 31814bf9cd..ea3ecd4ded 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -157,8 +157,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.systemQueueWorker .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -203,8 +203,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.dbQueueWorker .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -227,8 +227,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.deliverQueueWorker .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) - .on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -238,7 +238,7 @@ export class QueueProcessorService implements OnApplicationShutdown { autorun: false, concurrency: this.config.inboxJobConcurrency ?? 16, limiter: { - max: this.config.inboxJobPerSec ?? 16, + max: this.config.inboxJobPerSec ?? 32, duration: 1000, }, settings: { @@ -251,8 +251,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.inboxQueueWorker .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -275,8 +275,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.webhookDeliverQueueWorker .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) - .on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -304,8 +304,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); //#endregion @@ -327,8 +327,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.objectStorageQueueWorker .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) })) + .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); //#endregion diff --git a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts index 552b69d92d..03a0e951b3 100644 --- a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts @@ -1,5 +1,6 @@ import * as fs from 'node:fs'; import * as vm from 'node:vm'; +import * as crypto from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { ZipReader } from 'slacc'; import { DI } from '@/di-symbols.js'; @@ -72,7 +73,6 @@ export class ImportNotesProcessorService { } } - // Function was taken from Firefish and modified for our needs @bindThis private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise { type NotesMap = { @@ -378,7 +378,11 @@ export class ImportNotesProcessorService { return; } - if (toot.directMessage) return; + const followers = toot.to.some((str: string) => str.includes('/followers')); + + if (toot.directMessage || !toot.to.includes('https://www.w3.org/ns/activitystreams#Public') && !followers) return; + + const visibility = followers ? toot.cc.includes('https://www.w3.org/ns/activitystreams#Public') ? 'home' : 'followers' : 'public'; const date = new Date(toot.object.published); let text = undefined; @@ -417,7 +421,7 @@ export class ImportNotesProcessorService { } } - const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply }); + const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, visibility: visibility, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply }); if (toot.childNotes) this.queueService.createImportMastoToDbJob(user, toot.childNotes, createdNote.id); } @@ -469,7 +473,9 @@ export class ImportNotesProcessorService { for await (const file of post.object.attachment) { const slashdex = file.url.lastIndexOf('/'); - const name = file.url.substring(slashdex + 1); + const filename = file.url.substring(slashdex + 1); + const hash = crypto.createHash('md5').update(file.url).digest('base64url'); + const name = `${hash}-${filename}`; const [filePath, cleanup] = await createTemp(); const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: pleroFolder?.id }); @@ -484,6 +490,7 @@ export class ImportNotesProcessorService { user: user, path: filePath, name: name, + comment: file.name, folderId: pleroFolder?.id, }); files.push(driveFile); diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 2c9e2cf24f..8fa8320c8c 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -5,6 +5,7 @@ import * as crypto from 'node:crypto'; import { IncomingMessage } from 'node:http'; +import { format as formatURL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import fastifyAccepts from '@fastify/accepts'; import httpSignature from '@peertube/http-signature'; @@ -17,9 +18,13 @@ import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReact import * as url from '@/misc/prelude/url.js'; import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { QueueService } from '@/core/QueueService.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; +import { MetaService } from '@/core/MetaService.js'; import { UserKeypairService } from '@/core/UserKeypairService.js'; +import { InstanceActorService } from '@/core/InstanceActorService.js'; +import type { MiUserPublickey } from '@/models/UserPublickey.js'; import type { MiFollowing } from '@/models/Following.js'; import { countIf } from '@/misc/prelude/array.js'; import type { MiNote } from '@/models/Note.js'; @@ -31,12 +36,17 @@ import { IActivity } from '@/core/activitypub/type.js'; import { isPureRenote } from '@/misc/is-pure-renote.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FindOptionsWhere } from 'typeorm'; +import type Logger from '@/logger.js'; +import { LoggerService } from '@/core/LoggerService.js'; const ACTIVITY_JSON = 'application/activity+json; charset=utf-8'; const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'; @Injectable() export class ActivityPubServerService { + private logger: Logger; + private authlogger: Logger; + constructor( @Inject(DI.config) private config: Config, @@ -65,14 +75,20 @@ export class ActivityPubServerService { @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, + private metaService: MetaService, private utilityService: UtilityService, private userEntityService: UserEntityService, + private instanceActorService: InstanceActorService, private apRendererService: ApRendererService, + private apDbResolverService: ApDbResolverService, private queueService: QueueService, private userKeypairService: UserKeypairService, private queryService: QueryService, + private loggerService: LoggerService, ) { //this.createServer = this.createServer.bind(this); + this.logger = this.loggerService.getLogger('apserv', 'pink'); + this.authlogger = this.logger.createSubLogger('sigcheck'); } @bindThis @@ -99,6 +115,120 @@ export class ActivityPubServerService { return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); } + @bindThis + private async shouldRefuseGetRequest(request: FastifyRequest, reply: FastifyReply, userId: string | undefined = undefined): Promise { + if (!this.config.checkActivityPubGetSignature) return false; + + /* this code is inspired from the `inbox` function below, and + `queue/processors/InboxProcessorService` + + those pieces of code also check `digest`, and various bits from the + request body, but that only makes sense for requests with a body: + here we're validating GET requests + + this is also inspired by FireFish's `checkFetch` + */ + + /* tell any caching proxy that they should not cache these + responses: we wouldn't want the proxy to return a 403 to + someone presenting a valid signature, or return a cached + response body to someone we've blocked! + */ + reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); + + /* we always allow requests about our instance actor, because when + a remote instance needs to check our signature on a request we + sent, it will need to fetch information about the user that + signed it (which is our instance actor), and if we try to check + their signature on *that* request, we'll fetch *their* instance + actor... leading to an infinite recursion */ + if (userId) { + const instanceActor = await this.instanceActorService.getInstanceActor(); + + if (userId === instanceActor.id || userId === instanceActor.username) { + this.authlogger.debug(`${request.id} ${request.url} request to instance.actor, letting through`); + return false; + } + } + + let signature; + + try { + signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); + } catch (e) { + // not signed, or malformed signature: refuse + this.authlogger.warn(`${request.id} ${request.url} not signed, or malformed signature: refuse`); + reply.code(401); + return true; + } + + if (signature.params.headers.indexOf('host') === -1 + || request.headers.host !== this.config.host) { + // no destination host, or not us: refuse + this.authlogger.warn(`${request.id} ${request.url} no destination host, or not us: refuse`); + reply.code(401); + return true; + } + + const keyId = new URL(signature.keyId); + const keyHost = this.utilityService.toPuny(keyId.hostname); + + const meta = await this.metaService.fetch(); + if (this.utilityService.isBlockedHost(meta.blockedHosts, keyHost)) { + /* blocked instance: refuse (we don't care if the signature is + good, if they even pretend to be from a blocked instance, + they're out) */ + this.authlogger.warn(`${request.id} ${request.url} instance ${keyHost} is blocked: refuse`); + reply.code(401); + return true; + } + + // do we know the signer already? + let authUser: { + user: MiRemoteUser; + key: MiUserPublickey | null; + } | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId); + + if (authUser == null) { + /* keyId is often in the shape `${user.uri}#${keyname}`, try + fetching information about the remote user */ + const candidate = formatURL(keyId, { fragment: false }); + this.authlogger.info(`${request.id} ${request.url} we don't know the user for keyId ${keyId}, trying to fetch via ${candidate}`); + authUser = await this.apDbResolverService.getAuthUserFromApId(candidate); + } + + if (authUser?.key == null) { + // we can't figure out who the signer is, or we can't get their key: refuse + this.authlogger.warn(`${request.id} ${request.url} we can't figure out who the signer is, or we can't get their key: refuse`); + reply.code(401); + return true; + } + + let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + + if (!httpSignatureValidated) { + this.authlogger.info(`${request.id} ${request.url} failed to validate signature, re-fetching the key for ${authUser.user.uri}`); + // maybe they changed their key? refetch it + authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user); + + if (authUser.key != null) { + httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + } else { + this.authlogger.warn(`${request.id} ${request.url} failed to re-fetch key for ${authUser.user}`); + } + } + + if (!httpSignatureValidated) { + // bad signature: refuse + this.authlogger.info(`${request.id} ${request.url} failed to validate signature: refuse`); + reply.code(401); + return true; + } + + // all good, don't refuse + return false; + } + @bindThis private inbox(request: FastifyRequest, reply: FastifyReply) { let signature; @@ -138,7 +268,7 @@ export class ActivityPubServerService { return; } - const algo = match[1]; + const algo = match[1].toUpperCase(); const digestValue = match[2]; if (algo !== 'SHA-256') { @@ -172,6 +302,8 @@ export class ActivityPubServerService { request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, ) { + if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const userId = request.params.user; const cursor = request.query.cursor; @@ -195,13 +327,13 @@ export class ActivityPubServerService { //#region Check ff visibility const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followersVisibility === 'private') { reply.code(403); - reply.header('Cache-Control', 'public, max-age=30'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followersVisibility === 'followers') { reply.code(403); - reply.header('Cache-Control', 'public, max-age=30'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } //#endregion @@ -253,7 +385,7 @@ export class ActivityPubServerService { user.followersCount, `${partOf}?page=true`, ); - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -264,6 +396,8 @@ export class ActivityPubServerService { request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>, reply: FastifyReply, ) { + if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const userId = request.params.user; const cursor = request.query.cursor; @@ -287,13 +421,13 @@ export class ActivityPubServerService { //#region Check ff visibility const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followingVisibility === 'private') { reply.code(403); - reply.header('Cache-Control', 'public, max-age=30'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followingVisibility === 'followers') { reply.code(403); - reply.header('Cache-Control', 'public, max-age=30'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=30'); return; } //#endregion @@ -345,7 +479,7 @@ export class ActivityPubServerService { user.followingCount, `${partOf}?page=true`, ); - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -353,6 +487,8 @@ export class ActivityPubServerService { @bindThis private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) { + if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const userId = request.params.user; const user = await this.usersRepository.findOneBy({ @@ -370,8 +506,9 @@ export class ActivityPubServerService { order: { id: 'DESC' }, }); - const pinnedNotes = await Promise.all(pinings.map(pining => - this.notesRepository.findOneByOrFail({ id: pining.noteId }))); + const pinnedNotes = (await Promise.all(pinings.map(pining => + this.notesRepository.findOneByOrFail({ id: pining.noteId })))) + .filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility)); const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note))); @@ -383,7 +520,7 @@ export class ActivityPubServerService { renderedNotes, ); - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -396,6 +533,8 @@ export class ActivityPubServerService { }>, reply: FastifyReply, ) { + if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const userId = request.params.user; const sinceId = request.query.since_id; @@ -472,7 +611,7 @@ export class ActivityPubServerService { `${partOf}?page=true`, `${partOf}?page=true&since_id=000000000000000000000000`, ); - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); } @@ -485,15 +624,14 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); } @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - // addConstraintStrategy の型定義がおかしいため - (fastify.addConstraintStrategy as any)({ + fastify.addConstraintStrategy({ name: 'apOrHtml', storage() { const store = {} as any; @@ -551,6 +689,8 @@ export class ActivityPubServerService { // note fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { + if (await this.shouldRefuseGetRequest(request, reply)) return; + vary(reply.raw, 'Accept'); const note = await this.notesRepository.findOneBy({ @@ -574,13 +714,15 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return this.apRendererService.addContext(await this.apRendererService.renderNote(note, false)); }); // note activity fastify.get<{ Params: { note: string; } }>('/notes/:note/activity', async (request, reply) => { + if (await this.shouldRefuseGetRequest(request, reply)) return; + vary(reply.raw, 'Accept'); const note = await this.notesRepository.findOneBy({ @@ -595,7 +737,7 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.packActivity(note))); }); @@ -623,6 +765,8 @@ export class ActivityPubServerService { // publickey fastify.get<{ Params: { user: string; } }>('/users/:user/publickey', async (request, reply) => { + if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const userId = request.params.user; const user = await this.usersRepository.findOneBy({ @@ -638,7 +782,7 @@ export class ActivityPubServerService { const keypair = await this.userKeypairService.getUserKeypair(user.id); if (this.userEntityService.isLocalUser(user)) { - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair))); } else { @@ -648,6 +792,8 @@ export class ActivityPubServerService { }); fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { + if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const userId = request.params.user; const user = await this.usersRepository.findOneBy({ @@ -660,6 +806,8 @@ export class ActivityPubServerService { }); fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { + if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; + const user = await this.usersRepository.findOneBy({ usernameLower: request.params.user.toLowerCase(), host: IsNull(), @@ -672,6 +820,8 @@ export class ActivityPubServerService { // emoji fastify.get<{ Params: { emoji: string; } }>('/emojis/:emoji', async (request, reply) => { + if (await this.shouldRefuseGetRequest(request, reply)) return; + const emoji = await this.emojisRepository.findOneBy({ host: IsNull(), name: request.params.emoji, @@ -682,13 +832,15 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji))); }); // like fastify.get<{ Params: { like: string; } }>('/likes/:like', async (request, reply) => { + if (await this.shouldRefuseGetRequest(request, reply)) return; + const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like }); if (reaction == null) { @@ -703,13 +855,15 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note))); }); // follow fastify.get<{ Params: { follower: string; followee: string; } }>('/follows/:follower/:followee', async (request, reply) => { + if (await this.shouldRefuseGetRequest(request, reply)) return; + // This may be used before the follow is completed, so we do not // check if the following exists. @@ -729,13 +883,15 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); }); // follow fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => { + if (await this.shouldRefuseGetRequest(request, reply)) return; + // This may be used before the follow is completed, so we do not // check if the following exists and only check if the follow request exists. @@ -764,7 +920,7 @@ export class ActivityPubServerService { return; } - reply.header('Cache-Control', 'public, max-age=180'); + if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); }); diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 0b6a7dfe22..31479269b9 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { MemorySingleCache } from '@/misc/cache.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -96,6 +95,11 @@ export class NodeinfoServerService { metadata: { nodeName: meta.name, nodeDescription: meta.description, + nodeAdmins: [{ + name: meta.maintainerName, + email: meta.maintainerEmail, + }], + // deprecated maintainer: { name: meta.maintainerName, email: meta.maintainerEmail, @@ -109,10 +113,11 @@ export class NodeinfoServerService { disableRegistration: meta.disableRegistration, disableLocalTimeline: !basePolicies.ltlAvailable, disableGlobalTimeline: !basePolicies.gtlAvailable, + disableBubbleTimeline: !basePolicies.btlAvailable, emailRequiredForSignup: meta.emailRequiredForSignup, enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, + maxNoteTextLength: this.config.maxNoteLength, enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, proxyAccountName: proxyAccount ? proxyAccount.username : null, diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index fc5eece01f..52070b5157 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -32,6 +32,7 @@ import { AntennaChannelService } from './api/stream/channels/antenna.js'; import { ChannelChannelService } from './api/stream/channels/channel.js'; import { DriveChannelService } from './api/stream/channels/drive.js'; import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js'; +import { BubbleTimelineChannelService } from './api/stream/channels/bubble-timeline.js'; import { HashtagChannelService } from './api/stream/channels/hashtag.js'; import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js'; import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js'; @@ -77,6 +78,7 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; ChannelChannelService, DriveChannelService, GlobalTimelineChannelService, + BubbleTimelineChannelService, HashtagChannelService, RoleTimelineChannelService, HomeTimelineChannelService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 4d10e0fa4f..3b43b931ae 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -110,7 +110,7 @@ export class ServerService implements OnApplicationShutdown { fastify.register(this.activityPubServerService.createServer); fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.wellKnownServerService.createServer); - fastify.register(this.oauth2ProviderService.createServer); + fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' }); fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { const path = request.params.path; @@ -122,8 +122,8 @@ export class ServerService implements OnApplicationShutdown { return; } - const name = path.split('@')[0].replace('.webp', ''); - const host = path.split('@')[1]?.replace('.webp', ''); + const name = path.split('@')[0].replace(/\.webp$/i, ''); + const host = path.split('@')[1]?.replace(/\.webp$/i, ''); const emoji = await this.emojisRepository.findOneBy({ // `@.` is the spec of ReactionService.decodeReaction diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index 8fc3c96de6..c3eaf53a14 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -16,6 +16,7 @@ import * as Acct from '@/misc/acct.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; +import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; import type { FindOptionsWhere } from 'typeorm'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; @@ -30,6 +31,7 @@ export class WellKnownServerService { private nodeinfoServerService: NodeinfoServerService, private userEntityService: UserEntityService, + private oauth2ProviderService: OAuth2ProviderService, ) { //this.createServer = this.createServer.bind(this); } @@ -87,6 +89,10 @@ export class WellKnownServerService { return { links: this.nodeinfoServerService.getLinks() }; }); + fastify.get('/.well-known/oauth-authorization-server', async () => { + return this.oauth2ProviderService.generateRFC8414(); + }); + /* TODO fastify.get('/.well-known/change-password', async (request, reply) => { }); diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 66f171a5d8..56f804dee8 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -330,7 +330,8 @@ export class ApiCallService implements OnApplicationShutdown { } } - if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { + if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) + || (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) { throw new ApiError({ message: 'Your app does not have the necessary permissions to use this endpoint.', code: 'PERMISSION_DENIED', diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index e7014c1333..ed1b2d4377 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; +import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -277,6 +279,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; +import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; @@ -395,6 +398,8 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default }; const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default }; const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; +const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default }; +const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; @@ -648,6 +653,7 @@ const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create' const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; +const $notes_bubbleTimeline: Provider = { provide: 'ep:notes/bubble-timeline', useClass: ep___notes_bubbleTimeline.default }; const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default }; const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; @@ -770,6 +776,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_unsetUserAvatar, + $admin_unsetUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, @@ -1023,6 +1031,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $notes_favorites_delete, $notes_featured, $notes_globalTimeline, + $notes_bubbleTimeline, $notes_hybridTimeline, $notes_localTimeline, $notes_mentions, @@ -1139,6 +1148,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_unsetUserAvatar, + $admin_unsetUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, @@ -1392,6 +1403,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $notes_favorites_delete, $notes_featured, $notes_globalTimeline, + $notes_bubbleTimeline, $notes_hybridTimeline, $notes_localTimeline, $notes_mentions, diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index f0b3961f94..63379c8878 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -139,7 +139,7 @@ export class SignupApiService { code: invitationCode, }); - if (ticket == null) { + if (ticket == null || ticket.usedById != null) { reply.code(400); return; } diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index dc3a00617c..3b387d92ca 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -71,6 +71,10 @@ export class StreamingApiServerService { try { [user, app] = await this.authenticateService.authenticate(token); + + if (app !== null && !app.permission.some(p => p === 'read:account')) { + throw new AuthenticationError('Your app does not have necessary permissions to use websocket API.'); + } } catch (e) { if (e instanceof AuthenticationError) { socket.write([ diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 0d32b79900..f82bf257fc 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -4,6 +4,7 @@ */ import type { Schema } from '@/misc/json-schema.js'; +import { permissions } from 'misskey-js'; import { RolePolicies } from '@/core/RoleService.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; @@ -24,6 +25,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; +import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -277,6 +280,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; +import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; @@ -393,6 +397,8 @@ const eps = [ ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list], ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update], ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], + ['admin/unset-user-avatar', ep___admin_unsetUserAvatar], + ['admin/unset-user-banner', ep___admin_unsetUserBanner], ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], ['admin/drive/cleanup', ep___admin_drive_cleanup], ['admin/drive/files', ep___admin_drive_files], @@ -646,6 +652,7 @@ const eps = [ ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], ['notes/global-timeline', ep___notes_globalTimeline], + ['notes/bubble-timeline', ep___notes_bubbleTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], ['notes/local-timeline', ep___notes_localTimeline], ['notes/mentions', ep___notes_mentions], @@ -744,7 +751,7 @@ const eps = [ ['sponsors', ep___sponsors], ]; -export interface IEndpointMeta { +interface IEndpointMetaBase { readonly stability?: 'deprecated' | 'experimental' | 'stable'; readonly tags?: ReadonlyArray; @@ -843,6 +850,23 @@ export interface IEndpointMeta { readonly cacheSec?: number; } +export type IEndpointMeta = (Omit & { + requireCredential?: false, + requireAdmin?: false, + requireModerator?: false, +}) | (Omit & { + secure: true, +}) | (Omit & { + requireCredential: true, + kind: (typeof permissions)[number], +}) | (Omit & { + requireModerator: true, + kind: (typeof permissions)[number], +}) | (Omit & { + requireAdmin: true, + kind: (typeof permissions)[number], +}) + export interface IEndpoint { name: string; meta: IEndpointMeta; diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index be4fc82f0c..3484d6707a 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:abuse-user-reports', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 070e88f6f3..f54d567fff 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -46,12 +46,12 @@ export default class extends Endpoint { // eslint- private userEntityService: UserEntityService, private signupService: SignupService, ) { - super(meta, paramDef, async (ps, _me) => { + super(meta, paramDef, async (ps, _me, token) => { const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; const noUsers = (await this.usersRepository.countBy({ host: IsNull(), })) === 0; - if (!noUsers && !me?.isRoot) throw new Error('access denied'); + if ((!noUsers && !me?.isRoot) || token !== null) throw new Error('access denied'); const { account, secret } = await this.signupService.signup({ username: ps.username, diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index 60e928ccbe..52d8c8ce18 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -16,6 +16,7 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'write:admin:account', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts index 686341582b..93673453d6 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'read:admin:account', errors: { userNotFound: { @@ -23,6 +24,11 @@ export const meta = { id: 'cb865949-8af5-4062-a88c-ef55e8786d1d', }, }, + res: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index 17f792639b..041b10f9f7 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -15,6 +15,13 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:ad', + res: { + type: 'object', + optional: false, + nullable: false, + ref: 'Ad', + }, } as const; export const paramDef = { @@ -61,7 +68,18 @@ export default class extends Endpoint { // eslint- ad: ad, }); - return ad; + return { + id: ad.id, + expiresAt: ad.expiresAt.toISOString(), + startsAt: ad.startsAt.toISOString(), + dayOfWeek: ad.dayOfWeek, + url: ad.url, + imageUrl: ad.imageUrl, + priority: ad.priority, + ratio: ad.ratio, + place: ad.place, + memo: ad.memo, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts index 8097133a4c..5b18b347d3 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:ad', errors: { noSuchAd: { diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index 29eff89523..586c1f44db 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -14,6 +14,18 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:ad', + res: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'object', + optional: false, + nullable: false, + ref: 'Ad', + }, + }, } as const; export const paramDef = { @@ -22,7 +34,7 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, - publishing: { type: 'boolean', default: false }, + publishing: { type: 'boolean', default: null, nullable: true }, }, required: [], } as const; @@ -37,12 +49,25 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId); - if (ps.publishing) { + if (ps.publishing === true) { query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() }); + } else if (ps.publishing === false) { + query.andWhere('ad.expiresAt <= :now', { now: new Date() }).orWhere('ad.startsAt > :now', { now: new Date() }); } const ads = await query.limit(ps.limit).getMany(); - return ads; + return ads.map(ad => ({ + id: ad.id, + expiresAt: ad.expiresAt.toISOString(), + startsAt: ad.startsAt.toISOString(), + dayOfWeek: ad.dayOfWeek, + url: ad.url, + imageUrl: ad.imageUrl, + memo: ad.memo, + place: ad.place, + priority: ad.priority, + ratio: ad.ratio, + })); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index d065f9ec50..bf96e44b0c 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:ad', errors: { noSuchAd: { diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index 69c31a05eb..c9df70c76b 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:announcements', res: { type: 'object', diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts index 80ec281253..939333345e 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:announcements', errors: { noSuchAnnouncement: { diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 9630299a6e..429b138599 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -16,6 +16,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:announcements', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 717866aead..db6db8356d 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:announcements', errors: { noSuchAnnouncement: { diff --git a/packages/backend/src/server/api/endpoints/admin/approve-user.ts b/packages/backend/src/server/api/endpoints/admin/approve-user.ts index 0ea656ddaf..53002a71fd 100644 --- a/packages/backend/src/server/api/endpoints/admin/approve-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/approve-user.ts @@ -10,6 +10,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:approve-user', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts index ec143fcb53..4ac74253cc 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageAvatarDecorations', + kind: 'write:admin:avatar-decorations', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts index 6f1f386871..88977f801a 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageAvatarDecorations', + kind: 'write:admin:avatar-decorations', errors: { }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index d9c669377d..33122c3eef 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -17,6 +17,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageAvatarDecorations', + kind: 'read:admin:avatar-decorations', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts index 5ea9a40762..6211345f96 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageAvatarDecorations', + kind: 'write:admin:avatar-decorations', errors: { }, diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts index 9ef09b172e..2c82c2879d 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'write:admin:delete-account', res: { }, diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts index e47ecd81cf..7d33065f2e 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'write:admin:delete-all-files-of-a-user', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts index 8af44029c5..af2bb6b1ca 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:drive', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts index 75d689966f..a3b221284b 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:drive', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index ac8a70e3da..37fa439bcf 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:drive', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index 4e5320007e..3aeb3e45e3 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -16,6 +16,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:drive', errors: { noSuchFile: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 66ee4cab3b..1cd8125c52 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 58600c0eb3..0868e24948 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -16,6 +16,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', errors: { noSuchFile: { @@ -29,6 +30,8 @@ export const meta = { id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', }, }, + + ref: 'EmojiDetailed', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 9348e279f1..611b64be07 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -7,11 +7,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { EmojisRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import { DI } from '@/di-symbols.js'; import { DriveService } from '@/core/DriveService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { ApiError } from '../../../error.js'; @@ -20,6 +19,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', errors: { noSuchEmoji: { @@ -62,50 +62,43 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - private emojiEntityService: EmojiEntityService, - private idService: IdService, - private globalEventService: GlobalEventService, + private customEmojiService: CustomEmojiService, private driveService: DriveService, ) { super(meta, paramDef, async (ps, me) => { const emoji = await this.emojisRepository.findOneBy({ id: ps.emojiId }); - if (emoji == null) { throw new ApiError(meta.errors.noSuchEmoji); } - const isDuplicate = await this.emojisRepository.findOneBy({ name: emoji.name, host: IsNull() } ); - if (isDuplicate) throw new ApiError(meta.errors.duplicateName); - let driveFile: MiDriveFile; try { // Create file driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true }); } catch (e) { + // TODO: need to return Drive Error throw new ApiError(); } - const copied = await this.emojisRepository.insert({ - id: this.idService.gen(), - updatedAt: new Date(), + // Duplication Check + const isDuplicate = await this.customEmojiService.checkDuplicate(emoji.name); + if (isDuplicate) throw new ApiError(meta.errors.duplicateName); + + const addedEmoji = await this.customEmojiService.add({ + driveFile, name: emoji.name, + category: emoji.category, + aliases: emoji.aliases, host: null, - aliases: [], - originalUrl: driveFile.url, - publicUrl: driveFile.webpublicUrl ?? driveFile.url, - type: driveFile.webpublicType ?? driveFile.type, license: emoji.license, - }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + }, me); - this.globalEventService.publishBroadcastStream('emojiAdded', { - emoji: await this.emojiEntityService.packDetailed(copied.id), - }); - - return { - id: copied.id, - }; + return this.emojiEntityService.packDetailed(addedEmoji); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index e6c1bf317f..450695984a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 58aa0b9950..e1e6e7c2c4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', errors: { noSuchEmoji: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index 855ab8cd24..f3e0c1ef1f 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -17,6 +17,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'read:admin:emoji', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index ab16d86a3d..59e87253f6 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -17,6 +17,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'read:admin:emoji', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index a5dd6d5e3a..26dd43e926 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index 515053f57b..18961976f9 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index 8e834ad1dd..c680f2e2d4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts index 2dc9595a7e..47c692b613 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 04226d8953..550bb0052b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', errors: { noSuchEmoji: { diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts index b63f01bec3..57612850b4 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:federation', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts index 6dbfe3c4f5..0d061c685f 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:federation', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index 36ea390e45..c15fb83454 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:federation', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index 357bf83e87..188ab69532 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -16,6 +16,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:federation', } as const; export const paramDef = { @@ -23,8 +24,9 @@ export const paramDef = { properties: { host: { type: 'string' }, isSuspended: { type: 'boolean' }, + isNSFW: { type: 'boolean' }, }, - required: ['host', 'isSuspended'], + required: ['host'], } as const; @Injectable() @@ -44,23 +46,31 @@ export default class extends Endpoint { // eslint- throw new Error('instance not found'); } - await this.federatedInstanceService.update(instance.id, { - isSuspended: ps.isSuspended, - }); + if (ps.isSuspended != null) { + await this.federatedInstanceService.update(instance.id, { + isSuspended: ps.isSuspended, + }); - if (instance.isSuspended !== ps.isSuspended) { - if (ps.isSuspended) { - this.moderationLogService.log(me, 'suspendRemoteInstance', { - id: instance.id, - host: instance.host, - }); - } else { - this.moderationLogService.log(me, 'unsuspendRemoteInstance', { - id: instance.id, - host: instance.host, - }); + if (instance.isSuspended !== ps.isSuspended) { + if (ps.isSuspended) { + this.moderationLogService.log(me, 'suspendRemoteInstance', { + id: instance.id, + host: instance.host, + }); + } else { + this.moderationLogService.log(me, 'unsuspendRemoteInstance', { + id: instance.id, + host: instance.host, + }); + } } } + + if (ps.isNSFW != null) { + await this.federatedInstanceService.update(instance.id, { + isNSFW: ps.isNSFW, + }); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts index 4bd9e7de7f..0b50212119 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts @@ -11,8 +11,19 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, requireAdmin: true, + kind: 'read:admin:index-stats', tags: ['admin'], + res: { + type: 'array', + items: { + type: 'object', + properties: { + tablename: { type: 'string' }, + indexname: { type: 'string' }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts index f953b889a3..0d44b288cb 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts @@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, requireAdmin: true, + kind: 'read:admin:table-stats', tags: ['admin'], diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts index 6afa824703..1b437f718b 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts @@ -14,6 +14,26 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:user-ips', + res: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'object', + optional: false, + nullable: false, + properties: { + ip: { type: 'string' }, + createdAt: { + type: 'string', + optional: false, + nullable: false, + format: 'date-time', + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts index 4a22fd4824..396b84623f 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts @@ -18,6 +18,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:invite-codes', errors: { invalidDateTime: { @@ -33,13 +34,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, - properties: { - code: { - type: 'string', - optional: false, nullable: false, - example: 'GR6S02ERUA5VR', - }, - }, + ref: 'InviteCode', }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/invite/list.ts b/packages/backend/src/server/api/endpoints/admin/invite/list.ts index f25d3fcb33..d293dcadc6 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/list.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:invite-codes', res: { type: 'array', @@ -21,6 +22,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, + ref: 'InviteCode', }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index b1ba1633c9..4fd2a568ad 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'read:admin:meta', res: { type: 'object', @@ -147,6 +148,14 @@ export const meta = { type: 'string', }, }, + bannedEmailDomains: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, preservedUsernames: { type: 'array', optional: false, nullable: false, @@ -154,6 +163,13 @@ export const meta = { type: 'string', }, }, + bubbleInstances: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, hcaptchaSecretKey: { type: 'string', optional: false, nullable: true, @@ -275,6 +291,14 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + enableVerifymailApi: { + type: 'boolean', + optional: false, nullable: false, + }, + verifymailAuthKey: { + type: 'string', + optional: false, nullable: true, + }, enableChartsForRemoteUser: { type: 'boolean', optional: false, nullable: false, @@ -331,6 +355,86 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + backgroundImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + deeplAuthKey: { + type: 'string', + optional: false, nullable: true, + }, + deeplIsPro: { + type: 'boolean', + optional: false, nullable: false, + }, + defaultDarkTheme: { + type: 'string', + optional: false, nullable: true, + }, + defaultLightTheme: { + type: 'string', + optional: false, nullable: true, + }, + description: { + type: 'string', + optional: false, nullable: true, + }, + disableRegistration: { + type: 'boolean', + optional: false, nullable: false, + }, + impressumUrl: { + type: 'string', + optional: false, nullable: true, + }, + maintainerEmail: { + type: 'string', + optional: false, nullable: true, + }, + maintainerName: { + type: 'string', + optional: false, nullable: true, + }, + name: { + type: 'string', + optional: false, nullable: true, + }, + shortName: { + type: 'string', + optional: false, nullable: true, + }, + objectStorageS3ForcePathStyle: { + type: 'boolean', + optional: false, nullable: false, + }, + privacyPolicyUrl: { + type: 'string', + optional: false, nullable: true, + }, + repositoryUrl: { + type: 'string', + optional: false, nullable: false, + }, + summalyProxy: { + type: 'string', + optional: false, nullable: true, + }, + themeColor: { + type: 'string', + optional: false, nullable: true, + }, + tosUrl: { + type: 'string', + optional: false, nullable: true, + }, + uri: { + type: 'string', + optional: false, nullable: false, + }, + version: { + type: 'string', + optional: false, nullable: false, + }, }, }, } as const; @@ -402,6 +506,7 @@ export default class extends Endpoint { // eslint- silencedHosts: instance.silencedHosts, sensitiveWords: instance.sensitiveWords, preservedUsernames: instance.preservedUsernames, + bubbleInstances: instance.bubbleInstances, hcaptchaSecretKey: instance.hcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, @@ -436,11 +541,14 @@ export default class extends Endpoint { // eslint- deeplIsPro: instance.deeplIsPro, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, + enableVerifymailApi: instance.enableVerifymailApi, + verifymailAuthKey: instance.verifymailAuthKey, enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, enableServerMachineStats: instance.enableServerMachineStats, enableAchievements: instance.enableAchievements, enableIdenticonGeneration: instance.enableIdenticonGeneration, + bannedEmailDomains: instance.bannedEmailDomains, policies: { ...DEFAULT_POLICIES, ...instance.policies }, manifestJsonOverride: instance.manifestJsonOverride, enableFanoutTimeline: instance.enableFanoutTimeline, diff --git a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts index 2dff0e8d09..2a47abe03c 100644 --- a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts @@ -8,6 +8,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:nsfw-user', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index 4061e1b5df..ab69dfba96 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:promo', errors: { noSuchNote: { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts index c9142e9885..9912043c8b 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts @@ -13,6 +13,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:queue', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index 1515ae4c74..8473909103 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -13,6 +13,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:queue', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index febe0d07c6..19f7cb85c0 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -13,6 +13,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:queue', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts index 0cba5b4e25..d06780e044 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -13,6 +13,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:queue', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index 901195e9a5..189690b703 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:emoji', res: { type: 'object', diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index b675db2b89..d55dff7b0c 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:relays', errors: { invalidUrl: { diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts index 0633c57ed5..61ea287bff 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:relays', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts index 661b4243c4..8a6dd4e152 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:relays', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index a4a57d8e4e..506cd609ae 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -17,6 +17,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:reset-password', res: { type: 'object', diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index fb5ac7a335..26c4038b98 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -17,6 +17,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:resolve-abuse-user-report', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts index a0f3edd867..8eb3d2bf59 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:roles', errors: { noSuchRole: { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index 8451b1955f..de23d2fb11 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -13,6 +13,13 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'write:admin:roles', + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts index 7b989050eb..9e2968e317 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'write:admin:roles', errors: { noSuchRole: { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts index 3ed4b324dc..d3d1a10a69 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts @@ -14,6 +14,17 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:roles', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts index 5f0accab6f..ad4345e5a5 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:roles', errors: { noSuchRole: { @@ -23,6 +24,12 @@ export const meta = { id: '07dc7d34-c0d8-49b7-96c6-db3ce64ee0b3', }, }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts index 4c27583111..c11265252c 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:roles', errors: { noSuchRole: { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts index b4e7e29e90..203f749a6e 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts @@ -13,6 +13,7 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'write:admin:roles', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index 6031e2363e..74d5aae5d8 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -16,6 +16,7 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'write:admin:roles', errors: { noSuchRole: { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index b7f9aa0495..66f4d9d26b 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -18,6 +18,7 @@ export const meta = { requireCredential: false, requireAdmin: true, + kind: 'read:admin:roles', errors: { noSuchRole: { @@ -26,6 +27,20 @@ export const meta = { id: '224eff5e-2488-4b18-b3e7-f50d94421648', }, }, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', format: 'misskey:id' }, + createdAt: { type: 'string', format: 'date-time' }, + user: { ref: 'UserDetailed' }, + expiresAt: { type: 'string', format: 'date-time', nullable: true }, + }, + required: ['id', 'createdAt', 'user'], + }, + } } as const; export const paramDef = { @@ -78,7 +93,7 @@ export default class extends Endpoint { // eslint- id: assign.id, createdAt: this.idService.parse(assign.id).date.toISOString(), user: await this.userEntityService.pack(assign.user!, me, { detail: true }), - expiresAt: assign.expiresAt, + expiresAt: assign.expiresAt?.toISOString() ?? null, }))); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/send-email.ts b/packages/backend/src/server/api/endpoints/admin/send-email.ts index b9f2c6a6f1..d20aee656c 100644 --- a/packages/backend/src/server/api/endpoints/admin/send-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/send-email.ts @@ -12,6 +12,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:send-email', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts index 3169373b0e..374712f57d 100644 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts @@ -14,6 +14,7 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:server-info', tags: ['admin', 'meta'], diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts index f87a5a3574..f3601be9bb 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts @@ -14,7 +14,8 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireModerator: true, + requireAdmin: true, + kind: 'read:admin:show-moderation-log', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index b1cf24b6ac..ea22f9eeb9 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -16,6 +16,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:show-user', res: { type: 'object', diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index c7f717ff15..6267fb97b2 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -16,6 +16,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'read:admin:show-users', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts index ed1141da43..007bed5c03 100644 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts @@ -9,6 +9,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:silence-user', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 9464f4b677..a26fa81c13 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -21,6 +21,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:suspend-user', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts index 9c414ed55c..013e7771ba 100644 --- a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts @@ -8,6 +8,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:unnsfw-user', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts new file mode 100644 index 0000000000..8b22fad1d4 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:unset-user-avatar', +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + if (user.avatarId == null) return; + + await this.usersRepository.update(user.id, { + avatar: null, + avatarId: null, + avatarUrl: null, + avatarBlurhash: null, + }); + + this.moderationLogService.log(me, 'unsetUserAvatar', { + userId: user.id, + userUsername: user.username, + userHost: user.host, + fileId: user.avatarId, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts new file mode 100644 index 0000000000..5ec359c0ef --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:unset-user-banner', +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + if (user.bannerId == null) return; + + await this.usersRepository.update(user.id, { + banner: null, + bannerId: null, + bannerUrl: null, + bannerBlurhash: null, + }); + + this.moderationLogService.log(me, 'unsetUserBanner', { + userId: user.id, + userUsername: user.username, + userHost: user.host, + fileId: user.bannerId, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts index 7cfedca7de..5e514ccda6 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts @@ -8,6 +8,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:unsilence-user', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index 5e523bbc31..9c896f0e64 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:unsuspend-user', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index e1a1f3acb3..5c916fe340 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireAdmin: true, + kind: 'write:admin:meta', } as const; export const paramDef = { @@ -116,13 +117,17 @@ export const paramDef = { objectStorageS3ForcePathStyle: { type: 'boolean' }, enableIpLogging: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' }, + enableVerifymailApi: { type: 'boolean' }, + verifymailAuthKey: { type: 'string', nullable: true }, enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, enableServerMachineStats: { type: 'boolean' }, enableAchievements: { type: 'boolean' }, enableIdenticonGeneration: { type: 'boolean' }, serverRules: { type: 'array', items: { type: 'string' } }, + bannedEmailDomains: { type: 'array', items: { type: 'string' } }, preservedUsernames: { type: 'array', items: { type: 'string' } }, + bubbleInstances: { type: 'array', items: { type: 'string' } }, manifestJsonOverride: { type: 'string' }, enableFanoutTimeline: { type: 'boolean' }, enableFanoutTimelineDbFallback: { type: 'boolean' }, @@ -454,6 +459,18 @@ export default class extends Endpoint { // eslint- set.enableActiveEmailValidation = ps.enableActiveEmailValidation; } + if (ps.enableVerifymailApi !== undefined) { + set.enableVerifymailApi = ps.enableVerifymailApi; + } + + if (ps.verifymailAuthKey !== undefined) { + if (ps.verifymailAuthKey === '') { + set.verifymailAuthKey = null; + } else { + set.verifymailAuthKey = ps.verifymailAuthKey; + } + } + if (ps.enableChartsForRemoteUser !== undefined) { set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser; } @@ -482,6 +499,10 @@ export default class extends Endpoint { // eslint- set.preservedUsernames = ps.preservedUsernames; } + if (ps.bubbleInstances !== undefined) { + set.bubbleInstances = ps.bubbleInstances; + } + if (ps.manifestJsonOverride !== undefined) { set.manifestJsonOverride = ps.manifestJsonOverride; } @@ -514,6 +535,10 @@ export default class extends Endpoint { // eslint- set.notesPerOneAd = ps.notesPerOneAd; } + if (ps.bannedEmailDomains !== undefined) { + set.bannedEmailDomains = ps.bannedEmailDomains; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts index bfccc2a2a5..e582147e72 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -14,6 +14,7 @@ export const meta = { requireCredential: true, requireModerator: true, + kind: 'write:admin:user-note', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 9b5911800c..0bf2688b4a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -12,7 +12,8 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -70,7 +71,8 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private noteReadService: NoteReadService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -85,12 +87,18 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchAntenna); } - this.antennasRepository.update(antenna.id, { - isActive: true, - lastUsedAt: new Date(), - }); + // falseだった場合はアンテナの配信先が増えたことを通知したい + const needPublishEvent = !antenna.isActive; - let noteIds = await this.funoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId); + antenna.isActive = true; + antenna.lastUsedAt = new Date(); + this.antennasRepository.update(antenna.id, antenna); + + if (needPublishEvent) { + this.globalEventService.publishInternalEvent('antennaUpdated', antenna); + } + + let noteIds = await this.fanoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId); noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { return []; diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index a4a7fd2037..e0ef5d413a 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -12,6 +12,7 @@ export const meta = { tags: ['federation'], requireCredential: true, + kind: 'read:federation', limit: { duration: ms('1hour'), diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 6cdd617561..8ab16880fa 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -25,6 +25,7 @@ export const meta = { tags: ['federation'], requireCredential: true, + kind: 'read:account', limit: { duration: ms('1minute'), diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index fae4249c8a..006228ceee 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -4,17 +4,17 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, MiNote, NotesRepository } from '@/models/_.js'; +import type { ChannelsRepository, NotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { MiLocalUser } from '@/models/User.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -50,6 +50,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default }, required: ['channelId'], } as const; @@ -57,9 +58,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -69,14 +67,16 @@ export default class extends Endpoint { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private cacheService: CacheService, private activeUsersChart: ActiveUsersChart, + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const isRangeSpecified = untilId != null && sinceId != null; + + const serverSettings = await this.metaService.fetch(); const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, @@ -88,64 +88,48 @@ export default class extends Endpoint { // eslint- if (me) this.activeUsersChart.read(me); - if (isRangeSpecified || sinceId == null) { - const [ - userIdsWhoMeMuting, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - ]) : [new Set()]; - - let noteIds = await this.funoutTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); - - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - let timeline = await query.getMany(); - - timeline = timeline.filter(note => { - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - - return true; - }); - - // TODO: フィルタで件数が減った場合の埋め合わせ処理 - - timeline.sort((a, b) => a.id > b.id ? -1 : 1); - - if (timeline.length > 0) { - return await this.noteEntityService.packMany(timeline, me); - } - } + if (!serverSettings.enableFanoutTimeline) { + return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me); } - //#region fallback to database - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.channelId = :channelId', { channelId: channel.id }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - } - //#endregion - - const timeline = await query.limit(ps.limit).getMany(); - - return await this.noteEntityService.packMany(timeline, me); - //#endregion + return await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: true, + redisTimelines: [`channelTimeline:${channel.id}`], + excludePureRenotes: false, + dbFallback: async (untilId, sinceId, limit) => { + return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me); + }, + }); }); } + + private async getFromDb(ps: { + untilId: string | null, + sinceId: string | null, + limit: number, + channelId: string + }, me: MiLocalUser | null) { + //#region fallback to database + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.channelId = :channelId', { channelId: ps.channelId }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } + //#endregion + + return await query.limit(ps.limit).getMany(); + } } diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index cecaded20a..66ac8f664f 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -11,6 +11,23 @@ export const meta = { requireCredential: false, tags: ['meta'], + + res: { + type: 'object', + nullable: true, + properties: { + params: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + type: { type: 'string' }, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index c8beefa9c7..617ca65733 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -36,13 +36,33 @@ export const paramDef = { blocked: { type: 'boolean', nullable: true }, notResponding: { type: 'boolean', nullable: true }, suspended: { type: 'boolean', nullable: true }, - silenced: { type: "boolean", nullable: true }, + silenced: { type: 'boolean', nullable: true }, federating: { type: 'boolean', nullable: true }, subscribing: { type: 'boolean', nullable: true }, publishing: { type: 'boolean', nullable: true }, + nsfw: { type: 'boolean', nullable: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, offset: { type: 'integer', default: 0 }, - sort: { type: 'string' }, + sort: { + type: 'string', + nullable: true, + enum: [ + '+pubSub', + '-pubSub', + '+notes', + '-notes', + '+users', + '-users', + '+following', + '-following', + '+followers', + '-followers', + '+firstRetrievedAt', + '-firstRetrievedAt', + '+latestRequestReceivedAt', + '-latestRequestReceivedAt', + ], + }, }, required: [], } as const; @@ -103,18 +123,26 @@ export default class extends Endpoint { // eslint- } } - if (typeof ps.silenced === "boolean") { + if (typeof ps.nsfw === 'boolean') { + if (ps.nsfw) { + query.andWhere('instance.isNSFW = TRUE'); + } else { + query.andWhere('instance.isNSFW = FALSE'); + } + } + + if (typeof ps.silenced === 'boolean') { const meta = await this.metaService.fetch(true); if (ps.silenced) { if (meta.silencedHosts.length === 0) { return []; } - query.andWhere("instance.host IN (:...silences)", { + query.andWhere('instance.host IN (:...silences)', { silences: meta.silencedHosts, }); } else if (meta.silencedHosts.length > 0) { - query.andWhere("instance.host NOT IN (:...silences)", { + query.andWhere('instance.host NOT IN (:...silences)', { silences: meta.silencedHosts, }); } diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index 71eec11235..781c15e742 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -16,12 +16,9 @@ export const meta = { requireCredential: false, res: { - oneOf: [{ - type: 'object', - ref: 'FederationInstance', - }, { - type: 'null', - }], + type: 'object', + optional: false, nullable: true, + ref: 'FederationInstance', }, } as const; diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index e3ffea7b7e..262aa68776 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -18,6 +18,38 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + res: { + type: 'object', + optional: false, + nullable: false, + properties: { + topSubInstances: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'object', + optional: false, + nullable: false, + ref: 'FederationInstance', + }, + }, + otherFollowersCount: { type: 'number' }, + topPubInstances: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'object', + optional: false, + nullable: false, + ref: 'FederationInstance', + }, + }, + otherFollowingCount: { type: 'number' }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts index c0aa882088..e6198ff601 100644 --- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts +++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts @@ -11,7 +11,7 @@ import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['federation'], - requireCredential: true, + requireCredential: false, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts index d7b46cc666..cbe579eb6b 100644 --- a/packages/backend/src/server/api/endpoints/fetch-external-resources.ts +++ b/packages/backend/src/server/api/endpoints/fetch-external-resources.ts @@ -14,6 +14,7 @@ export const meta = { tags: ['meta'], requireCredential: true, + secure: true, limit: { duration: ms('1hour'), @@ -32,6 +33,18 @@ export const meta = { id: '693ba8ba-b486-40df-a174-72f8279b56a4', }, }, + + res: { + type: 'object', + properties: { + type: { + type: 'string', + }, + data: { + type: 'string', + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 37859d8330..b2dee83fe9 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -16,6 +16,18 @@ export const meta = { requireCredential: false, allowGet: true, cacheSec: 60 * 3, + + res: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + type: 'object', + }, + } + } + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index 4fa65ac9aa..674f323734 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -27,6 +27,12 @@ export const meta = { errors: { }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Flash', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index cbab3a83a4..cea4234065 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { GalleryPostsRepository } from '@/models/_.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; export const meta = { tags: ['gallery'], @@ -27,25 +28,49 @@ export const meta = { export const paramDef = { type: 'object', - properties: {}, + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + untilId: { type: 'string', format: 'misskey:id' }, + }, required: [], } as const; @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export + private galleryPostsRankingCache: string[] = []; + private galleryPostsRankingCacheLastFetchedAt = 0; + constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, private galleryPostEntityService: GalleryPostEntityService, + private featuredService: FeaturedService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.galleryPostsRepository.createQueryBuilder('post') - .andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) }) - .andWhere('post.likedCount > 0') - .orderBy('post.likedCount', 'DESC'); + let postIds: string[]; + if (this.galleryPostsRankingCacheLastFetchedAt !== 0 && (Date.now() - this.galleryPostsRankingCacheLastFetchedAt < 1000 * 60 * 30)) { + postIds = this.galleryPostsRankingCache; + } else { + postIds = await this.featuredService.getGalleryPostsRanking(100); + this.galleryPostsRankingCache = postIds; + this.galleryPostsRankingCacheLastFetchedAt = Date.now(); + } - const posts = await query.limit(10).getMany(); + postIds.sort((a, b) => a > b ? -1 : 1); + if (ps.untilId) { + postIds = postIds.filter(id => id < ps.untilId!); + } + postIds = postIds.slice(0, ps.limit); + + if (postIds.length === 0) { + return []; + } + + const query = this.galleryPostsRepository.createQueryBuilder('post') + .where('post.id IN (:...postIds)', { postIds: postIds }); + + const posts = await query.getMany(); return await this.galleryPostEntityService.packMany(posts, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 561b2492ab..cc424261b4 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js'; +import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -57,6 +58,7 @@ export default class extends Endpoint { // eslint- @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, + private featuredService: FeaturedService, private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { @@ -88,6 +90,11 @@ export default class extends Endpoint { // eslint- userId: me.id, }); + // ランキング更新 + if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { + await this.featuredService.updateGalleryPostsRanking(post.id, 1); + } + this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1); }); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index 832b62282f..caa4d45553 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -6,6 +6,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js'; +import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js'; +import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -49,6 +51,9 @@ export default class extends Endpoint { // eslint- @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, + + private featuredService: FeaturedService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId }); @@ -68,6 +73,11 @@ export default class extends Endpoint { // eslint- // Delete like await this.galleryLikesRepository.delete(exist.id); + // ランキング更新 + if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { + await this.featuredService.updateGalleryPostsRanking(post.id, -1); + } + this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1); }); } diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index 8a61168f25..737d637b7e 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -16,6 +16,16 @@ export const meta = { requireCredential: false, allowGet: true, cacheSec: 60 * 1, + res: { + type: 'object', + optional: false, nullable: false, + properties: { + count: { + type: 'number', + nullable: false, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index c0530bf392..c24e049180 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -14,6 +14,7 @@ export const meta = { tags: ['account'], requireCredential: true, + kind: "read:account", res: { type: 'object', diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 6033ce5dd4..4161553d28 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -33,6 +33,16 @@ export const meta = { id: '798d6847-b1ed-4f9c-b1f9-163c42655995', }, }, + + res: { + type: 'object', + nullable: false, + optional: false, + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index a6d05507ed..325d54d196 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -37,6 +37,140 @@ export const meta = { id: 'bf32b864-449b-47b8-974e-f9a5468546f1', }, }, + + res: { + type: 'object', + nullable: false, + optional: false, + properties: { + rp: { + type: 'object', + properties: { + id: { + type: 'string', + nullable: true, + }, + }, + }, + user: { + type: 'object', + properties: { + id: { + type: 'string', + }, + name: { + type: 'string', + }, + displayName: { + type: 'string', + }, + }, + }, + challenge: { + type: 'string', + }, + pubKeyCredParams: { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + }, + alg: { + type: 'number', + }, + }, + }, + }, + timeout: { + type: 'number', + nullable: true, + }, + excludeCredentials: { + type: 'array', + nullable: true, + items: { + type: 'object', + properties: { + id: { + type: 'string', + }, + type: { + type: 'string', + }, + transports: { + type: 'array', + items: { + type: 'string', + enum: [ + "ble", + "cable", + "hybrid", + "internal", + "nfc", + "smart-card", + "usb", + ], + }, + }, + }, + }, + }, + authenticatorSelection: { + type: 'object', + nullable: true, + properties: { + authenticatorAttachment: { + type: 'string', + enum: [ + "cross-platform", + "platform", + ], + }, + requireResidentKey: { + type: 'boolean', + }, + userVerification: { + type: 'string', + enum: [ + "discouraged", + "preferred", + "required", + ], + }, + }, + }, + attestation: { + type: 'string', + nullable: true, + enum: [ + "direct", + "enterprise", + "indirect", + "none", + ], + }, + extensions: { + type: 'object', + nullable: true, + properties: { + appid: { + type: 'string', + nullable: true, + }, + credProps: { + type: 'boolean', + nullable: true, + }, + hmacCreateSecret: { + type: 'boolean', + nullable: true, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 9b3ae74f86..15e50c49f3 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -27,6 +27,19 @@ export const meta = { id: '78d6c839-20c9-4c66-b90a-fc0542168b48', }, }, + + res: { + type: 'object', + nullable: false, + optional: false, + properties: { + qr: { type: 'string' }, + url: { type: 'string' }, + secret: { type: 'string' }, + label: { type: 'string' }, + issuer: { type: 'string' }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 09f6540a77..ef89f93181 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -13,6 +13,37 @@ export const meta = { requireCredential: true, secure: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + name: { + type: 'string', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + lastUsedAt: { + type: 'string', + format: 'date-time', + }, + permission: { + type: 'array', + uniqueItems: true, + items: { + type: 'string' + }, + } + }, + }, + }, } as const; export const paramDef = { @@ -50,7 +81,7 @@ export default class extends Endpoint { // eslint- id: token.id, name: token.name ?? token.app?.name, createdAt: this.idService.parse(token.id).date.toISOString(), - lastUsedAt: token.lastUsedAt, + lastUsedAt: token.lastUsedAt?.toISOString(), permission: token.permission, }))); }); diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 32061c2aa4..a0ed371fb8 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -14,6 +14,36 @@ export const meta = { requireCredential: true, secure: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + name: { + type: 'string', + }, + callbackUrl: { + type: 'string', + nullable: true, + }, + permission: { + type: 'array', + uniqueItems: true, + items: { + type: 'string' + }, + }, + isAuthorized: { + type: 'boolean', + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts index 303c0a7f87..3580d6ba1b 100644 --- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -11,6 +11,7 @@ import { MetaService } from '@/core/MetaService.js'; export const meta = { requireCredential: true, prohibitMoved: true, + kind: 'write:account', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 86b726e054..f3ba720c2b 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -64,6 +64,10 @@ export const meta = { id: 'b234a14e-9ebe-4581-8000-074b3c215962', }, }, + + res: { + type: 'object', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index 29fa0a29cc..79a81cb73f 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -9,6 +9,11 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, + kind: 'read:account', + + res: { + type: 'object', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index 5b460b45d6..d9b26cab2c 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -10,6 +10,7 @@ import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, + kind: 'read:account', errors: { noSuchKey: { @@ -18,6 +19,10 @@ export const meta = { id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a', }, }, + + res: { + type: 'object', + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts index 74d896e650..bb471284c9 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts @@ -6,6 +6,7 @@ import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, + kind: 'read:account', secure: false, diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index e8c28298ef..c373410256 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -10,6 +10,7 @@ import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, + kind: 'read:account', errors: { noSuchKey: { @@ -18,6 +19,10 @@ export const meta = { id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', }, }, + + res: { + type: 'object', + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index 8953ee5d3d..a91dcd9543 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -9,6 +9,11 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, + kind: 'read:account', + + res: { + type: 'object', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 04e120d752..ad203d5203 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -9,6 +9,7 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, + kind: 'read:account', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index ba8100b547..9cbe271b91 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -12,6 +12,7 @@ import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, + kind: 'write:account', errors: { noSuchKey: { diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts index 1ff994b82c..0aca2a26fe 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -10,6 +10,28 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, secure: true, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + scopes: { + type: 'array', + items: { + type: 'array', + items: { + type: 'string', + } + } + }, + domain: { + type: 'string', + nullable: true, + }, + }, + }, + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 58bb450bce..c61d5b8727 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -9,6 +9,7 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, + kind: 'write:account', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 139bede7bc..f82e3f9b28 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -12,8 +12,17 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, - secure: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Signin', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index d3b4649783..090b07be3c 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -41,6 +41,11 @@ export const meta = { id: 'a2defefb-f220-8849-0af6-17f816099323', }, }, + + res: { + type: 'object', + ref: 'UserDetailed', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 63f9991b22..22079de042 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -4,7 +4,7 @@ */ import RE2 from 're2'; -import * as mfm from 'mfm-js'; +import * as mfm from '@sharkey/sfm-js'; import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { JSDOM } from 'jsdom'; @@ -135,6 +135,11 @@ export const meta = { }, } as const; +const muteWords = { type: 'array', items: { oneOf: [ + { type: 'array', items: { type: 'string' } }, + { type: 'string' }, +] } } as const; + export const paramDef = { type: 'object', properties: { @@ -145,12 +150,14 @@ export const paramDef = { listenbrainz: { ...listenbrainzSchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, - avatarDecorations: { type: 'array', maxItems: 1, items: { + avatarDecorations: { type: 'array', maxItems: 16, items: { type: 'object', properties: { id: { type: 'string', format: 'misskey:id' }, angle: { type: 'number', nullable: true, maximum: 0.5, minimum: -0.5 }, flipH: { type: 'boolean', nullable: true }, + offsetX: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, + offsetY: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, }, required: ['id'], } }, @@ -185,9 +192,11 @@ export const paramDef = { receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, autoSensitive: { type: 'boolean' }, - ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, + followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, + followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, - mutedWords: { type: 'array' }, + mutedWords: muteWords, + hardMutedWords: muteWords, mutedInstances: { type: 'array', items: { type: 'string', } }, @@ -250,17 +259,22 @@ export default class extends Endpoint { // eslint- if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.listenbrainz !== undefined) profileUpdates.listenbrainz = ps.listenbrainz; - if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; - if (ps.mutedWords !== undefined) { + if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility; + if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; + + function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える - const length = JSON.stringify(ps.mutedWords).length; - if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { + const length = JSON.stringify(mutedWords).length; + if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } + } - // validate regular expression syntax - ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { - const regexp = x.match(/^\/(.+)\/(.*)$/); + function validateMuteWordRegex(mutedWords: (string[] | string)[]) { + for (const mutedWord of mutedWords) { + if (typeof mutedWord !== 'string') continue; + + const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); try { @@ -268,11 +282,21 @@ export default class extends Endpoint { // eslint- } catch (err) { throw new ApiError(meta.errors.invalidRegexp); } - }); + } + } + + if (ps.mutedWords !== undefined) { + checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.mutedWords); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } + if (ps.hardMutedWords !== undefined) { + checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.hardMutedWords); + profileUpdates.hardMutedWords = ps.hardMutedWords; + } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; @@ -341,16 +365,20 @@ export default class extends Endpoint { // eslint- if (ps.avatarDecorations) { const decorations = await this.avatarDecorationService.getAll(true); - const myRoles = await this.roleService.getUserRoles(user.id); + const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const allRoles = await this.roleService.getRoles(); const decorationIds = decorations .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .map(d => d.id); + if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); + updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, angle: d.angle ?? 0, flipH: d.flipH ?? false, + offsetX: d.offsetX ?? 0, + offsetY: d.offsetY ?? 0, })); } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index f00dba4a85..bdc9f9ea8b 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -27,6 +27,33 @@ export const meta = { id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', }, }, + + res: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + name: { type: 'string' }, + on: { + type: 'array', + items: { + type: 'string', + enum: webhookEventTypes, + } + }, + url: { type: 'string' }, + secret: { type: 'string' }, + active: { type: 'boolean' }, + latestSentAt: { type: 'string', format: 'date-time', nullable: true }, + latestStatus: { type: 'integer', nullable: true }, + }, + }, } as const; export const paramDef = { @@ -73,7 +100,17 @@ export default class extends Endpoint { // eslint- this.globalEventService.publishInternalEvent('webhookCreated', webhook); - return webhook; + return { + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt?.toISOString(), + latestStatus: webhook.latestStatus, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index aa8921fe24..afb2d0509e 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { webhookEventTypes } from '@/models/Webhook.js'; import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -14,6 +15,36 @@ export const meta = { requireCredential: true, kind: 'read:account', + + res: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + name: { type: 'string' }, + on: { + type: 'array', + items: { + type: 'string', + enum: webhookEventTypes, + } + }, + url: { type: 'string' }, + secret: { type: 'string' }, + active: { type: 'boolean' }, + latestSentAt: { type: 'string', format: 'date-time', nullable: true }, + latestStatus: { type: 'integer', nullable: true }, + }, + } + } } as const; export const paramDef = { @@ -33,7 +64,19 @@ export default class extends Endpoint { // eslint- userId: me.id, }); - return webhooks; + return webhooks.map(webhook => ( + { + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt?.toISOString(), + latestStatus: webhook.latestStatus, + } + )); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index f1294bb5c8..5c6dd908b4 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { webhookEventTypes } from '@/models/Webhook.js'; import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -23,6 +24,33 @@ export const meta = { id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098', }, }, + + res: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + name: { type: 'string' }, + on: { + type: 'array', + items: { + type: 'string', + enum: webhookEventTypes, + } + }, + url: { type: 'string' }, + secret: { type: 'string' }, + active: { type: 'boolean' }, + latestSentAt: { type: 'string', format: 'date-time', nullable: true }, + latestStatus: { type: 'integer', nullable: true }, + }, + }, } as const; export const paramDef = { @@ -49,7 +77,17 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchWebhook); } - return webhook; + return { + id: webhook.id, + userId: webhook.userId, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + active: webhook.active, + latestSentAt: webhook.latestSentAt?.toISOString(), + latestStatus: webhook.latestStatus, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts index 94836283fa..4f37f2f4bb 100644 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -19,6 +19,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canInvite', + kind: 'write:invite-codes', errors: { exceededCreateLimit: { @@ -31,13 +32,7 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - properties: { - code: { - type: 'string', - optional: false, nullable: false, - example: 'GR6S02ERUA5VR', - }, - }, + ref: 'InviteCode', }, } as const; diff --git a/packages/backend/src/server/api/endpoints/invite/delete.ts b/packages/backend/src/server/api/endpoints/invite/delete.ts index 3b57775739..d84430a49f 100644 --- a/packages/backend/src/server/api/endpoints/invite/delete.ts +++ b/packages/backend/src/server/api/endpoints/invite/delete.ts @@ -15,6 +15,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canInvite', + kind: 'write:invite-codes', errors: { noSuchCode: { diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts index 1f4190c948..fc3bb9bdc2 100644 --- a/packages/backend/src/server/api/endpoints/invite/limit.ts +++ b/packages/backend/src/server/api/endpoints/invite/limit.ts @@ -16,6 +16,7 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canInvite', + kind: 'read:invite-codes', res: { type: 'object', diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts index 06139b6806..6734f27e14 100644 --- a/packages/backend/src/server/api/endpoints/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/invite/list.ts @@ -9,13 +9,13 @@ import type { RegistrationTicketsRepository } from '@/models/_.js'; import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; export const meta = { tags: ['meta'], requireCredential: true, requireRolePolicy: 'canInvite', + kind: 'read:invite-codes', res: { type: 'array', @@ -23,6 +23,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, + ref: 'InviteCode', }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 8c8fdde066..1d0c102c9d 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -7,7 +7,6 @@ import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import JSON5 from 'json5'; import type { AdsRepository, UsersRepository } from '@/models/_.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { MetaService } from '@/core/MetaService.js'; @@ -172,20 +171,34 @@ export const meta = { type: 'object', optional: false, nullable: false, properties: { - place: { + id: { type: 'string', optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', }, url: { type: 'string', optional: false, nullable: false, format: 'url', }, + place: { + type: 'string', + optional: false, nullable: false, + }, + ratio: { + type: 'number', + optional: false, nullable: false, + }, imageUrl: { type: 'string', optional: false, nullable: false, format: 'url', }, + dayOfWeek: { + type: 'integer', + optional: false, nullable: false, + }, }, }, }, @@ -258,6 +271,33 @@ export const meta = { }, }, }, + backgroundImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + impressumUrl: { + type: 'string', + optional: false, nullable: true, + }, + logoImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + privacyPolicyUrl: { + type: 'string', + optional: false, nullable: true, + }, + serverRules: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, + themeColor: { + type: 'string', + optional: false, nullable: true, + }, }, }, } as const; @@ -334,7 +374,7 @@ export default class extends Endpoint { // eslint- iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, + maxNoteTextLength: this.config.maxNoteLength, // クライアントの手間を減らすためあらかじめJSONに変換しておく defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null, defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null, diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts index 98c317346f..1b70b85b07 100644 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ b/packages/backend/src/server/api/endpoints/my/apps.ts @@ -13,6 +13,7 @@ export const meta = { tags: ['account', 'app'], requireCredential: true, + kind: 'read:account', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts new file mode 100644 index 0000000000..c5e3a5a5f7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts @@ -0,0 +1,128 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import type { NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../error.js'; +import { CacheService } from '@/core/CacheService.js'; +import { MetaService } from '@/core/MetaService.js'; + +export const meta = { + tags: ['notes'], + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, + + errors: { + btlDisabled: { + message: 'Bubble timeline has been disabled.', + code: 'BTL_DISABLED', + id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6c', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + withFiles: { type: 'boolean', default: false }, + withBots: { type: 'boolean', default: true }, + withRenotes: { type: 'boolean', default: true }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + private roleService: RoleService, + private activeUsersChart: ActiveUsersChart, + private cacheService: CacheService, + private metaService: MetaService, + ) { + super(meta, paramDef, async (ps, me) => { + const policies = await this.roleService.getUserPolicies(me ? me.id : null); + const instance = await this.metaService.fetch(); + if (!policies.btlAvailable) { + throw new ApiError(meta.errors.btlDisabled); + } + + const [ + followings, + ] = me ? await Promise.all([ + this.cacheService.userFollowingsCache.fetch(me.id), + ]) : [undefined]; + + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.visibility = \'public\'') + .andWhere('note.channelId IS NULL') + .andWhere('note.userHost IN (:...hosts)', { hosts: instance.bubbleInstances }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + + if (!ps.withBots) query.andWhere('user.isBot = FALSE'); + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.where('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.where('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } + //#endregion + + let timeline = await query.limit(ps.limit).getMany(); + + timeline = timeline.filter(note => { + if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false; + return true; + }); + + process.nextTick(() => { + if (me) { + this.activeUsersChart.read(me); + } + }); + + return await this.noteEntityService.packMany(timeline, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index 6086f99c92..0de5a14a93 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -72,7 +72,7 @@ describe('api:notes/create', () => { .toBe(INVALID); }); - test('over 100 characters cw', async () => { + test('over 500 characters cw', async () => { expect(v({ text: 'Body', cw: await tooLong })) .toBe(INVALID); }); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 49287f4de9..ac0a7f3b51 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -11,7 +11,7 @@ import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesR import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; import type { MiChannel } from '@/models/Channel.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import type { Config } from '@/config.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; @@ -82,6 +82,12 @@ export const meta = { id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', }, + maxLength: { + message: 'You tried posting a note which is too long.', + code: 'MAX_LENGTH', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16', + }, + cannotCreateAlreadyExpiredPoll: { message: 'Poll is already expired.', code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', @@ -121,7 +127,7 @@ export const paramDef = { visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, - cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, @@ -136,7 +142,6 @@ export const paramDef = { text: { type: 'string', minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true, }, fileIds: { @@ -184,6 +189,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -203,6 +211,10 @@ export default class extends Endpoint { // eslint- private noteCreateService: NoteCreateService, ) { super(meta, paramDef, async (ps, me) => { + if (ps.text && (ps.text.length > this.config.maxNoteLength)) { + throw new ApiError(meta.errors.maxLength); + } + let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { visibleUsers = await this.usersRepository.findBy({ @@ -262,7 +274,7 @@ export default class extends Endpoint { // eslint- if (renote.channelId && renote.channelId !== ps.channelId) { // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する - const renoteChannel = await this.channelsRepository.findOneById(renote.channelId); + const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); if (renoteChannel == null) { // リノートしたいノートが書き込まれているチャンネルが無い throw new ApiError(meta.errors.noSuchChannel); diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 49fa4c3bd6..0c9c0d3baf 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -6,7 +6,7 @@ import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesR import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; import type { MiChannel } from '@/models/Channel.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import type { Config } from '@/config.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEditService } from '@/core/NoteEditService.js'; @@ -123,6 +123,24 @@ export const meta = { code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', id: '33510210-8452-094c-6227-4a6c05d99f00', }, + + cannotQuoteaQuoteOfCurrentPost: { + message: 'Cannot quote a quote of edited note.', + code: 'CANNOT_QUOTE_A_QUOTE_OF_EDITED_NOTE', + id: '33510210-8452-094c-6227-4a6c05d99f01', + }, + + cannotQuoteCurrentPost: { + message: 'Cannot quote the current note.', + code: 'CANNOT_QUOTE_THE_CURRENT_NOTE', + id: '33510210-8452-094c-6227-4a6c05d99f02', + }, + + maxLength: { + message: 'You tried posting a note which is too long.', + code: 'MAX_LENGTH', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16', + }, }, } as const; @@ -139,7 +157,7 @@ export const paramDef = { format: 'misskey:id', }, }, - cw: { type: 'string', nullable: true, minLength: 1, maxLength: 250 }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, @@ -151,7 +169,6 @@ export const paramDef = { text: { type: 'string', minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true, }, fileIds: { @@ -193,7 +210,6 @@ export const paramDef = { text: { type: 'string', minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false, }, }, @@ -224,6 +240,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -243,6 +262,9 @@ export default class extends Endpoint { // eslint- private noteEditService: NoteEditService, ) { super(meta, paramDef, async (ps, me) => { + if (ps.text && (ps.text.length > this.config.maxNoteLength)) { + throw new ApiError(meta.errors.maxLength); + } let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { visibleUsers = await this.usersRepository.findBy({ @@ -268,6 +290,11 @@ export default class extends Endpoint { // eslint- } let renote: MiNote | null = null; + + if (ps.renoteId === ps.editId) { + throw new ApiError(meta.errors.cannotQuoteCurrentPost); + } + if (ps.renoteId != null) { // Fetch renote to note renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); @@ -278,6 +305,10 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.cannotReRenote); } + if (renote.renoteId === ps.editId) { + throw new ApiError(meta.errors.cannotQuoteaQuoteOfCurrentPost); + } + // Check blocking if (renote.userId !== me.id) { const blockExist = await this.blockingsRepository.exist({ diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index c456874309..31b8d1ad2d 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -9,6 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { FeaturedService } from '@/core/FeaturedService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { CacheService } from '@/core/CacheService.js'; export const meta = { tags: ['notes'], @@ -47,6 +49,7 @@ export default class extends Endpoint { // eslint- @Inject(DI.notesRepository) private notesRepository: NotesRepository, + private cacheService: CacheService, private noteEntityService: NoteEntityService, private featuredService: FeaturedService, ) { @@ -64,16 +67,24 @@ export default class extends Endpoint { // eslint- } } - if (noteIds.length === 0) { - return []; - } - noteIds.sort((a, b) => a > b ? -1 : 1); if (ps.untilId) { noteIds = noteIds.filter(id => id < ps.untilId!); } noteIds = noteIds.slice(0, ps.limit); + if (noteIds.length === 0) { + return []; + } + + const [ + userIdsWhoMeMuting, + userIdsWhoBlockingMe, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]) : [new Set(), new Set()]; + const query = this.notesRepository.createQueryBuilder('note') .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') @@ -83,10 +94,14 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); - const notes = await query.getMany(); - notes.sort((a, b) => a.id > b.id ? -1 : 1); + const notes = (await query.getMany()).filter(note => { + if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - // TODO: ミュート等考慮 + return true; + }); + + notes.sort((a, b) => a.id > b.id ? -1 : 1); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 767c31d434..13cfb31ad0 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -5,26 +5,27 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository, MiNote, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], requireCredential: true, + kind: 'read:account', res: { type: 'array', @@ -42,6 +43,12 @@ export const meta = { code: 'STL_DISABLED', id: '620763f4-f621-4533-ab33-0577a1a3c342', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f', + }, }, } as const; @@ -53,6 +60,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, @@ -78,10 +86,10 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, - private funoutTimelineService: FunoutTimelineService, private queryService: QueryService, private userFollowingService: UserFollowingService, private metaService: MetaService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -92,10 +100,12 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.stlDisabled); } + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { - return await this.getFromDb({ + const timeline = await this.getFromDb({ untilId, sinceId, limit: ps.limit, @@ -106,106 +116,63 @@ export default class extends Endpoint { // eslint- withReplies: ps.withReplies, withBots: ps.withBots, }, me); - } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]); - - let noteIds: string[]; - let shouldFallbackToDb = false; - - if (ps.withFiles) { - const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ - `homeTimelineWithFiles:${me.id}`, - 'localTimelineWithFiles', - ], untilId, sinceId); - noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); - } else if (ps.withReplies) { - const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([ - `homeTimeline:${me.id}`, - 'localTimeline', - 'localTimelineWithReplies', - ], untilId, sinceId); - noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds])); - } else { - const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ - `homeTimeline:${me.id}`, - 'localTimeline', - ], untilId, sinceId); - noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); - shouldFallbackToDb = htlNoteIds.length === 0; - } - - noteIds.sort((a, b) => a > b ? -1 : 1); - noteIds = noteIds.slice(0, ps.limit); - - shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); - - let redisTimeline: MiNote[] = []; - - if (!shouldFallbackToDb) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - redisTimeline = await query.getMany(); - - redisTimeline = redisTimeline.filter(note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - - if (!ps.withBots && note.user?.isBot) return false; - - return true; - }); - - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } - - if (redisTimeline.length > 0) { process.nextTick(() => { this.activeUsersChart.read(me); }); - return await this.noteEntityService.packMany(redisTimeline, me); - } else { - if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db - return await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - withBots: ps.withBots, - }, me); - } else { - return []; - } + return await this.noteEntityService.packMany(timeline, me); } + + let timelineConfig: FanoutTimelineName[]; + + if (ps.withFiles) { + timelineConfig = [ + `homeTimelineWithFiles:${me.id}`, + 'localTimelineWithFiles', + ]; + } else if (ps.withReplies) { + timelineConfig = [ + `homeTimeline:${me.id}`, + 'localTimeline', + 'localTimelineWithReplies', + ]; + } else { + timelineConfig = [ + `homeTimeline:${me.id}`, + 'localTimeline', + ]; + } + + const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + redisTimelines: timelineConfig, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + excludeBots: !ps.withBots, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withReplies: ps.withReplies, + withBots: ps.withBots, + }, me), + }); + + process.nextTick(() => { + this.activeUsersChart.read(me); + }); + + return redisTimeline; }); } @@ -309,12 +276,6 @@ export default class extends Endpoint { // eslint- if (!ps.withBots) query.andWhere('user.isBot = FALSE'); //#endregion - const timeline = await query.limit(ps.limit).getMany(); - - process.nextTick(() => { - this.activeUsersChart.read(me); - }); - - return await this.noteEntityService.packMany(timeline, me); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 73566aa45d..8db7d25e03 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiNote, NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -13,11 +13,10 @@ import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -39,6 +38,12 @@ export const meta = { code: 'LTL_DISABLED', id: '45a6eb02-7695-4393-b023-dd3be9aaaefd', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793', + }, }, } as const; @@ -49,10 +54,10 @@ export const paramDef = { withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, withBots: { type: 'boolean', default: true }, - excludeNsfw: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, }, @@ -70,7 +75,7 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, private metaService: MetaService, ) { @@ -83,10 +88,12 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.ltlDisabled); } + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { - return await this.getFromDb({ + const timeline = await this.getFromDb({ untilId, sinceId, limit: ps.limit, @@ -94,90 +101,48 @@ export default class extends Endpoint { // eslint- withReplies: ps.withReplies, withBots: ps.withBots, }, me); - } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]) : [new Set(), new Set(), new Set()]; - - let noteIds: string[]; - - if (ps.withFiles) { - noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId); - } else { - const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([ - 'localTimeline', - 'localTimelineWithReplies', - ], untilId, sinceId); - noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds])); - noteIds.sort((a, b) => a > b ? -1 : 1); - } - - noteIds = noteIds.slice(0, ps.limit); - - let redisTimeline: MiNote[] = []; - - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - redisTimeline = await query.getMany(); - - redisTimeline = redisTimeline.filter(note => { - if (me && (note.userId === me.id)) { - return true; - } - if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false; - if (!ps.withBots && note.user?.isBot) return false; - if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - - return true; - }); - - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } - - if (redisTimeline.length > 0) { process.nextTick(() => { if (me) { this.activeUsersChart.read(me); } }); - return await this.noteEntityService.packMany(redisTimeline, me); - } else { - if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db - return await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - withFiles: ps.withFiles, - withReplies: ps.withReplies, - withBots: ps.withBots, - }, me); - } else { - return []; - } + return await this.noteEntityService.packMany(timeline, me); } + + const timeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + redisTimelines: + ps.withFiles ? ['localTimelineWithFiles'] + : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] + : me ? ['localTimeline', `localTimelineWithReplyTo:${me.id}`] + : ['localTimeline'], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + excludeBots: !ps.withBots, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + withFiles: ps.withFiles, + withReplies: ps.withReplies, + withBots: ps.withBots, + }, me), + }); + + process.nextTick(() => { + if (me) { + this.activeUsersChart.read(me); + } + }); + + return timeline; }); } @@ -221,14 +186,6 @@ export default class extends Endpoint { // eslint- if (!ps.withBots) query.andWhere('user.isBot = FALSE'); - const timeline = await query.limit(ps.limit).getMany(); - - process.nextTick(() => { - if (me) { - this.activeUsersChart.read(me); - } - }); - - return await this.noteEntityService.packMany(timeline, me); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 6fab024d17..2317f8f7b2 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -16,6 +16,7 @@ export const meta = { tags: ['notes'], requireCredential: true, + kind: 'read:account', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index af7ff8bdcd..90af29a695 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -14,6 +14,7 @@ export const meta = { tags: ['notes'], requireCredential: true, + kind: 'read:account', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index bc33d6948c..89e05fd57e 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -13,6 +13,8 @@ import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { UtilityService } from '@/core/UtilityService.js'; export const meta = { tags: ['notes', 'hashtags'], @@ -73,23 +75,32 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private metaService: MetaService, + private cacheService: CacheService, + private utilityService: UtilityService, ) { super(meta, paramDef, async (ps, me) => { + const meta = await this.metaService.fetch(true); + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.visibility = \'public\'') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - const meta = await this.metaService.fetch(true); - if (!meta.enableBotTrending) query.andWhere('user.isBot = FALSE'); this.queryService.generateVisibilityQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me); + const [ + followings, + ] = me ? await Promise.all([ + this.cacheService.userFollowingsCache.fetch(me.id), + ]) : [undefined]; + try { if (ps.tag) { if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); @@ -140,7 +151,15 @@ export default class extends Endpoint { // eslint- } // Search notes - const notes = await query.limit(ps.limit).getMany(); + let notes = await query.limit(ps.limit).getMany(); + + notes = notes.filter(note => { + if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false; + if (note.user?.isSuspended) return false; + if (this.utilityService.isBlockedHost(meta.blockedHosts, note.userHost)) return false; + if (this.utilityService.isSilencedHost(meta.silencedHosts, note.userHost)) return false; + return true; + }); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 5bb8196543..b3107f6754 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { GetterService } from '@/server/api/GetterService.js'; +import { DI } from '@/di-symbols.js'; +import type { NotesRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -40,14 +42,27 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + private noteEntityService: NoteEntityService, - private getterService: GetterService, + private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const note = await this.getterService.getNote(ps.noteId).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw err; - }); + const query = await this.notesRepository.createQueryBuilder('note') + .where('note.id = :noteId', { noteId: ps.noteId }); + + this.queryService.generateVisibilityQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } + + const note = await query.getOne(); + + if (note === null) { + throw new ApiError(meta.errors.noSuchNote); + } return await this.noteEntityService.pack(note, me, { detail: true, diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index b5fd47723c..20faea566d 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -12,6 +12,7 @@ export const meta = { tags: ['notes'], requireCredential: true, + kind: 'read:account', res: { type: 'object', diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 036993508f..3dcebe7e29 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiNote, NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -13,16 +13,16 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; export const meta = { tags: ['notes'], requireCredential: true, + kind: 'read:account', res: { type: 'array', @@ -43,6 +43,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, @@ -66,7 +67,7 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private userFollowingService: UserFollowingService, private queryService: QueryService, private metaService: MetaService, @@ -78,7 +79,7 @@ export default class extends Endpoint { // eslint- const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { - return await this.getFromDb({ + const timeline = await this.getFromDb({ untilId, sinceId, limit: ps.limit, @@ -89,83 +90,56 @@ export default class extends Endpoint { // eslint- withRenotes: ps.withRenotes, withBots: ps.withBots, }, me); + + process.nextTick(() => { + this.activeUsersChart.read(me); + }); + + return await this.noteEntityService.packMany(timeline, me); } const [ followings, - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, ] = await Promise.all([ this.cacheService.userFollowingsCache.fetch(me.id), - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), ]); - let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); - - let redisTimeline: MiNote[] = []; - - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - redisTimeline = await query.getMany(); - - redisTimeline = redisTimeline.filter(note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } + const timeline = this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + noteFilter: note => { if (note.reply && note.reply.visibility === 'followers') { if (!Object.hasOwn(followings, note.reply.userId)) return false; } if (!ps.withBots && note.user?.isBot) return false; return true; - }); + }, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + withBots: ps.withBots, + }, me), + }); - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } + process.nextTick(() => { + this.activeUsersChart.read(me); + }); - if (redisTimeline.length > 0) { - process.nextTick(() => { - this.activeUsersChart.read(me); - }); - - return await this.noteEntityService.packMany(redisTimeline, me); - } else { - if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db - return await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - withBots: ps.withBots, - }, me); - } else { - return []; - } - } + return timeline; }); } @@ -275,12 +249,6 @@ export default class extends Endpoint { // eslint- if (!ps.withBots) query.andWhere('user.isBot = FALSE'); //#endregion - const timeline = await query.limit(ps.limit).getMany(); - - process.nextTick(() => { - this.activeUsersChart.read(me); - }); - - return await this.noteEntityService.packMany(timeline, me); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index a1561c944c..698c37b616 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -17,10 +17,15 @@ export const meta = { tags: ['notes'], requireCredential: true, + kind: 'read:account', res: { type: 'object', optional: false, nullable: false, + properties: { + sourceLang: { type: 'string' }, + text: { type: 'string' }, + }, }, errors: { diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index dbc3875597..71c2b8054e 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -5,24 +5,24 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { MiNote, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['notes', 'lists'], requireCredential: true, + kind: 'read:account', res: { type: 'array', @@ -52,6 +52,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, @@ -81,7 +82,7 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private cacheService: CacheService, private idService: IdService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, private metaService: MetaService, ) { @@ -101,7 +102,7 @@ export default class extends Endpoint { // eslint- const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { - return await this.getFromDb(list, { + const timeline = await this.getFromDb(list, { untilId, sinceId, limit: ps.limit, @@ -111,73 +112,37 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, }, me); - } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]); - - let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId); - noteIds = noteIds.slice(0, ps.limit); - - let redisTimeline: MiNote[] = []; - - if (noteIds.length > 0) { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - redisTimeline = await query.getMany(); - - redisTimeline = redisTimeline.filter(note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - - return true; - }); - - redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1); - } - - if (redisTimeline.length > 0) { this.activeUsersChart.read(me); - return await this.noteEntityService.packMany(redisTimeline, me); - } else { - if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db - return await this.getFromDb(list, { - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me); - } else { - return []; - } + + await this.noteEntityService.packMany(timeline, me); } + + const timeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb(list, { + untilId, + sinceId, + limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me), + }); + + this.activeUsersChart.read(me); + + return timeline; }); } @@ -271,10 +236,6 @@ export default class extends Endpoint { // eslint- } //#endregion - const timeline = await query.limit(ps.limit).getMany(); - - this.activeUsersChart.read(me); - - return await this.noteEntityService.packMany(timeline, me); + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index 7d07c92178..f427939a7a 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -15,6 +15,7 @@ export const meta = { tags: ['notes'], requireCredential: true, + kind: 'write:account', errors: { noSuchNote: { diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts index d1de73ad32..d40e937d4e 100644 --- a/packages/backend/src/server/api/endpoints/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -13,6 +13,17 @@ export const meta = { tags: ['role'], requireCredential: true, + kind: 'read:account', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index daa9affc20..4ce3fc8908 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -11,13 +11,14 @@ import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['role', 'notes'], requireCredential: true, + kind: 'read:account', errors: { noSuchRole: { @@ -66,7 +67,7 @@ export default class extends Endpoint { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -84,7 +85,7 @@ export default class extends Endpoint { // eslint- return []; } - let noteIds = await this.funoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); + let noteIds = await this.fanoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId); noteIds = noteIds.slice(0, ps.limit); if (noteIds.length === 0) { diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts index 2afa0e7b7f..6bfe52bb1a 100644 --- a/packages/backend/src/server/api/endpoints/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/roles/show.ts @@ -22,6 +22,12 @@ export const meta = { id: 'de5502bf-009a-4639-86c1-fec349e46dcb', }, }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Role', + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index caaa3735e9..d304d075b2 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -24,6 +24,25 @@ export const meta = { id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5', }, }, + + res: { + type: 'array', + items: { + type: 'object', + nullable: false, + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + user: { + type: 'object', + ref: 'User' + }, + }, + required: ['id', 'user'], + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index c8cb63e6b3..079f2d7f1d 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -15,6 +15,53 @@ export const meta = { cacheSec: 60 * 1, tags: ['meta'], + res: { + type: 'object', + optional: false, nullable: false, + properties: { + machine: { + type: 'string', + nullable: false, + }, + cpu: { + type: 'object', + nullable: false, + properties: { + model: { + type: 'string', + nullable: false, + }, + cores: { + type: 'number', + nullable: false, + }, + }, + }, + mem: { + type: 'object', + properties: { + total: { + type: 'number', + nullable: false, + }, + }, + }, + fs: { + type: 'object', + nullable: false, + properties: { + total: { + type: 'number', + nullable: false, + }, + used: { + type: 'number', + nullable: false, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 9ab062326d..bb50048d94 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -14,6 +14,7 @@ export const meta = { tags: ['account'], requireCredential: true, + secure: true, description: 'Register to receive push notifications.', diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts index 126299e3f7..15d3df8587 100644 --- a/packages/backend/src/server/api/endpoints/sw/show-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts @@ -12,6 +12,7 @@ export const meta = { tags: ['account'], requireCredential: true, + secure: true, description: 'Check push notification registration exists.', diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts index a1a97df0be..7bf59784a2 100644 --- a/packages/backend/src/server/api/endpoints/sw/update-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts @@ -13,6 +13,7 @@ export const meta = { tags: ['account'], requireCredential: true, + secure: true, description: 'Update push notification registration.', diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 6d6d44f752..949867c572 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -12,6 +12,30 @@ export const meta = { description: 'Endpoint for testing input validation.', requireCredential: false, + + res: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id' + }, + required: { + type: 'boolean', + }, + string: { + type: 'string', + }, + default: { + type: 'string', + }, + nullableDefault: { + type: 'string', + default: 'hello', + nullable: true, + }, + } + } } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts index e4845d57bf..3a584a819a 100644 --- a/packages/backend/src/server/api/endpoints/users/achievements.ts +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -9,7 +9,22 @@ import type { UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { - requireCredential: true, + requireCredential: false, + + res: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + unlockedAt: { + type: 'number', + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts index dec0b7a122..7243aa3b3e 100644 --- a/packages/backend/src/server/api/endpoints/users/featured-notes.ts +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -9,6 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { FeaturedService } from '@/core/FeaturedService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; export const meta = { tags: ['notes'], @@ -46,8 +48,16 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private featuredService: FeaturedService, + private cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { + const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set(); + + // early return if me is blocked by requesting user + if (userIdsWhoBlockingMe.has(ps.userId)) { + return []; + } + let noteIds = await this.featuredService.getPerUserNotesRanking(ps.userId, 50); noteIds.sort((a, b) => a > b ? -1 : 1); @@ -60,6 +70,12 @@ export default class extends Endpoint { // eslint- return []; } + const [ + userIdsWhoMeMuting, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + ]) : [new Set()]; + const query = this.notesRepository.createQueryBuilder('note') .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') @@ -69,10 +85,14 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); - const notes = await query.getMany(); - notes.sort((a, b) => a.id > b.id ? -1 : 1); + const notes = (await query.getMany()).filter(note => { + if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; - // TODO: ミュート等考慮 + return true; + }); + + notes.sort((a, b) => a.id > b.id ? -1 : 1); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index b22fd2ff7a..5706e46b96 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -93,11 +93,11 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followersVisibility === 'private') { if (me == null || (me.id !== user.id)) { throw new ApiError(meta.errors.forbidden); } - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followersVisibility === 'followers') { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 03487275a3..794fb04f10 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -42,6 +42,12 @@ export const meta = { code: 'FORBIDDEN', id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba', }, + + birthdayInvalid: { + message: 'Birthday date format is invalid.', + code: 'BIRTHDAY_DATE_FORMAT_INVALID', + id: 'a2b007b9-4782-4eba-abd3-93b05ed4130d', + }, }, } as const; @@ -59,6 +65,8 @@ export const paramDef = { nullable: true, description: 'The local host is represented with `null`.', }, + + birthday: { type: 'string', nullable: true }, }, anyOf: [ { required: ['userId'] }, @@ -93,11 +101,11 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followingVisibility === 'private') { if (me == null || (me.id !== user.id)) { throw new ApiError(meta.errors.forbidden); } - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followingVisibility === 'followers') { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { @@ -117,6 +125,21 @@ export default class extends Endpoint { // eslint- .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); + if (ps.birthday) { + try { + const d = new Date(ps.birthday); + d.setHours(0, 0, 0, 0); + const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; + const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile'); + birthdayUserQuery.select('user_profile.userId') + .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`); + + query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`); + } catch (err) { + throw new ApiError(meta.errors.birthdayInvalid); + } + } + const followings = await query .limit(ps.limit) .getMany(); diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts index 4eb37c3e43..fa2e3338b8 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -18,6 +18,7 @@ import { UserListService } from '@/core/UserListService.js'; export const meta = { requireCredential: true, prohibitMoved: true, + kind: 'write:account', res: { type: 'object', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts index 2ecf0a1256..864cdc2ee0 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts @@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, + kind: 'write:account', errors: { noSuchList: { message: 'No such user list.', diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts index ae8b4e9b81..985141515e 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts @@ -25,6 +25,35 @@ export const meta = { id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', }, }, + + res: { + type: 'array', + items: { + type: 'object', + nullable: false, + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + user: { + type: 'object', + ref: 'User', + }, + withReplies: { + type: 'boolean', + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts index 23611ab8c4..d51d57343e 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts @@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: true, + kind: 'write:account', errors: { noSuchList: { message: 'No such user list.', diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index c3d72c3ba8..b485126ed8 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -5,17 +5,18 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; -import type { MiNote, NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { QueryService } from '@/core/QueryService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; -import { ApiError } from '../../error.js'; +import { MetaService } from '@/core/MetaService.js'; +import { MiLocalUser } from '@/models/User.js'; +import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['users', 'notes'], @@ -36,6 +37,12 @@ export const meta = { code: 'NO_SUCH_USER', id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', + }, }, } as const; @@ -51,8 +58,8 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default withFiles: { type: 'boolean', default: false }, - excludeNsfw: { type: 'boolean', default: false }, }, required: ['userId'], } as const; @@ -60,9 +67,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -70,125 +74,130 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, private cacheService: CacheService, private idService: IdService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineEndpointService: FanoutTimelineEndpointService, + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const isRangeSpecified = untilId != null && sinceId != null; const isSelf = me && (me.id === ps.userId); - if (isRangeSpecified || sinceId == null) { - const [ - userIdsWhoMeMuting, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - ]) : [new Set()]; + const serverSettings = await this.metaService.fetch(); - const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([ - this.funoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId), - ps.withReplies ? this.funoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), - ps.withChannelNotes ? this.funoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]), - ]); + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); - let noteIds = Array.from(new Set([ - ...noteIdsRes, - ...repliesNoteIdsRes, - ...channelNoteIdsRes, - ])); - noteIds.sort((a, b) => a > b ? -1 : 1); - noteIds = noteIds.slice(0, ps.limit); - - if (noteIds.length > 0) { - const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId); - - const query = this.notesRepository.createQueryBuilder('note') - .where('note.id IN (:...noteIds)', { noteIds: noteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - let timeline = await query.getMany(); - - timeline = timeline.filter(note => { - if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; - - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (ps.withRenotes === false) return false; - } - } - - if (ps.withFiles && note.fileIds.length === 0) return false; - if (!ps.withReplies && note.replyId) return false; - - if (note.channel?.isSensitive && !isSelf) return false; - if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; - if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; - - return true; - }); - - // TODO: フィルタで件数が減った場合の埋め合わせ処理 - - timeline.sort((a, b) => a.id > b.id ? -1 : 1); - - if (timeline.length > 0) { - return await this.noteEntityService.packMany(timeline, me); - } + // early return if me is blocked by requesting user + if (me != null) { + const userIdsWhoBlockingMe = await this.cacheService.userBlockedCache.fetch(me.id); + if (userIdsWhoBlockingMe.has(ps.userId)) { + return []; } } - //#region fallback to database - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.userId = :userId', { userId: ps.userId }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('note.channel', 'channel') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); + if (!serverSettings.enableFanoutTimeline) { + const timeline = await this.getFromDb({ + untilId, + sinceId, + limit: ps.limit, + userId: ps.userId, + withChannelNotes: ps.withChannelNotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me); - if (ps.withChannelNotes) { - if (!isSelf) query.andWhere(new Brackets(qb => { - qb.orWhere('note.channelId IS NULL'); - qb.orWhere('channel.isSensitive = false'); - })); - } else { - query.andWhere('note.channelId IS NULL'); + return await this.noteEntityService.packMany(timeline, me); } - this.queryService.generateVisibilityQuery(query, me); - if (me) { - this.queryService.generateMutedUserQuery(query, me, { id: ps.userId }); - this.queryService.generateBlockedUserQuery(query, me); - } + const redisTimelines: FanoutTimelineName[] = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`]; - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } + if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`); + if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`); - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :userId', { userId: ps.userId }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + const isFollowing = me && Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId); - if (!ps.withReplies) { - query.andWhere('note.replyId IS NULL'); - } + const timeline = await this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + redisTimelines, + useDbFallback: true, + ignoreAuthorFromMute: true, + excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies + excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files + excludePureRenotes: !ps.withRenotes, + noteFilter: note => { + if (note.channel?.isSensitive && !isSelf) return false; + if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; + if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; - const timeline = await query.limit(ps.limit).getMany(); + return true; + }, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + userId: ps.userId, + withChannelNotes: ps.withChannelNotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me), + }); - return await this.noteEntityService.packMany(timeline, me); - //#endregion + return timeline; }); } + + private async getFromDb(ps: { + untilId: string | null, + sinceId: string | null, + limit: number, + userId: string, + withChannelNotes: boolean, + withFiles: boolean, + withRenotes: boolean, + }, me: MiLocalUser | null) { + const isSelf = me && (me.id === ps.userId); + + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.userId = :userId', { userId: ps.userId }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('note.channel', 'channel') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + if (ps.withChannelNotes) { + if (!isSelf) query.andWhere(new Brackets(qb => { + qb.orWhere('note.channelId IS NULL'); + qb.orWhere('channel.isSensitive = false'); + })); + } else { + query.andWhere('note.channelId IS NULL'); + } + + this.queryService.generateVisibilityQuery(query, me); + if (me) { + this.queryService.generateMutedUserQuery(query, me, { id: ps.userId }); + this.queryService.generateBlockedUserQuery(query, me); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.userId != :userId', { userId: ps.userId }); + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); + })); + } + + return await query.limit(ps.limit).getMany(); + } } diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 326042ed3d..26b61c9fb2 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -11,6 +11,7 @@ export const meta = { tags: ['users'], requireCredential: true, + kind: 'read:account', description: 'Show the different kinds of relations between the authenticated user and the specified user(s).', diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 649802744f..bdaf78758b 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -20,6 +20,7 @@ export const meta = { tags: ['users'], requireCredential: true, + kind: 'write:report-abuse', description: 'File a report.', diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 9571e14cbe..cce7f798fe 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Entity } from 'megalodon'; -import mfm from 'mfm-js'; +import mfm from '@sharkey/sfm-js'; import { DI } from '@/di-symbols.js'; import { MfmService } from '@/core/MfmService.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts index 61713b3415..efb39ef939 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts @@ -1,5 +1,5 @@ import { Entity } from 'megalodon'; -import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from '@/const.js'; +import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import type { Config } from '@/config.js'; import type { MiMeta } from '@/models/Meta.js'; @@ -35,7 +35,7 @@ export async function getInstance( max_featured_tags: 20, }, statuses: { - max_characters: MAX_NOTE_TEXT_LENGTH, + max_characters: config.maxNoteLength, max_media_attachments: 16, characters_reserved_per_url: response.uri.length, }, diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 4f972d3f7e..0e71510b48 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -4,7 +4,7 @@ */ import type { Config } from '@/config.js'; -import endpoints from '../endpoints.js'; +import endpoints, { IEndpoint } from '../endpoints.js'; import { errors as basicErrors } from './errors.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; @@ -33,16 +33,17 @@ export function genOpenapiSpec(config: Config) { schemas: schemas, securitySchemes: { - ApiKeyAuth: { - type: 'apiKey', - in: 'body', - name: 'i', + bearerAuth: { + type: 'http', + scheme: 'bearer', }, }, }, }; - for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { + // 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する + const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[]; + for (const endpoint of copiedEndpoints) { const errors = {} as any; if (endpoint.meta.errors) { @@ -58,6 +59,11 @@ export function genOpenapiSpec(config: Config) { const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; + + if (endpoint.meta.secure) { + desc += '**Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.\n'; + } + desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; if (endpoint.meta.kind) { const kind = endpoint.meta.kind; @@ -79,6 +85,13 @@ export function genOpenapiSpec(config: Config) { schema.required = [...schema.required ?? [], 'file']; } + if (schema.required && schema.required.length <= 0) { + // 空配列は許可されない + schema.required = undefined; + } + + const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1); + const info = { operationId: endpoint.name, summary: endpoint.name, @@ -92,17 +105,19 @@ export function genOpenapiSpec(config: Config) { } : {}), ...(endpoint.meta.requireCredential ? { security: [{ - ApiKeyAuth: [], + bearerAuth: [], }], } : {}), - requestBody: { - required: true, - content: { - [requestType]: { - schema, + ...(hasBody ? { + requestBody: { + required: true, + content: { + [requestType]: { + schema, + }, }, }, - }, + } : {}), responses: { ...(endpoint.meta.res ? { '200': { @@ -118,6 +133,11 @@ export function genOpenapiSpec(config: Config) { description: 'OK (without any results)', }, }), + ...(endpoint.meta.res?.optional === true || endpoint.meta.res?.nullable === true ? { + '204': { + description: 'OK (without any results)', + }, + } : {}), '400': { description: 'Client error', content: { @@ -190,6 +210,7 @@ export function genOpenapiSpec(config: Config) { }; spec.paths['/' + endpoint.name] = { + ...(endpoint.meta.allowGet ? { get: info } : {}), post: info, }; } diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index 1a1d973e56..2716f5f162 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -7,10 +7,16 @@ import type { Schema } from '@/misc/json-schema.js'; import { refs } from '@/misc/json-schema.js'; export function convertSchemaToOpenApiSchema(schema: Schema) { - const res: any = schema; + // optional, refはスキーマ定義に含まれないので分離しておく + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { optional, ref, ...res }: any = schema; if (schema.type === 'object' && schema.properties) { - res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); + const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); + if (required.length > 0) { + // 空配列は許可されない + res.required = required; + } for (const k of Object.keys(schema.properties)) { res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 8fd106c10c..3fc3f4d31a 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -8,6 +8,7 @@ import { bindThis } from '@/decorators.js'; import { HybridTimelineChannelService } from './channels/hybrid-timeline.js'; import { LocalTimelineChannelService } from './channels/local-timeline.js'; import { HomeTimelineChannelService } from './channels/home-timeline.js'; +import { BubbleTimelineChannelService } from './channels/bubble-timeline.js'; import { GlobalTimelineChannelService } from './channels/global-timeline.js'; import { MainChannelService } from './channels/main.js'; import { ChannelChannelService } from './channels/channel.js'; @@ -19,6 +20,7 @@ import { AntennaChannelService } from './channels/antenna.js'; import { DriveChannelService } from './channels/drive.js'; import { HashtagChannelService } from './channels/hashtag.js'; import { RoleTimelineChannelService } from './channels/role-timeline.js'; +import { type MiChannelService } from './channel.js'; @Injectable() export class ChannelsService { @@ -28,6 +30,7 @@ export class ChannelsService { private localTimelineChannelService: LocalTimelineChannelService, private hybridTimelineChannelService: HybridTimelineChannelService, private globalTimelineChannelService: GlobalTimelineChannelService, + private bubbleTimelineChannelService: BubbleTimelineChannelService, private userListChannelService: UserListChannelService, private hashtagChannelService: HashtagChannelService, private roleTimelineChannelService: RoleTimelineChannelService, @@ -41,13 +44,14 @@ export class ChannelsService { } @bindThis - public getChannelService(name: string) { + public getChannelService(name: string): MiChannelService { switch (name) { case 'main': return this.mainChannelService; case 'homeTimeline': return this.homeTimelineChannelService; case 'localTimeline': return this.localTimelineChannelService; case 'hybridTimeline': return this.hybridTimelineChannelService; case 'globalTimeline': return this.globalTimelineChannelService; + case 'bubbleTimeline': return this.bubbleTimelineChannelService; case 'userList': return this.userListChannelService; case 'hashtag': return this.hashtagChannelService; case 'roleTimeline': return this.roleTimelineChannelService; diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index 2d8fec30b1..a89fbcc5e5 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -36,6 +36,7 @@ export default class Connection { public userIdsWhoMeMuting: Set = new Set(); public userIdsWhoBlockingMe: Set = new Set(); public userIdsWhoMeMutingRenotes: Set = new Set(); + public userMutedInstances: Set = new Set(); private fetchIntervalId: NodeJS.Timeout | null = null; constructor( @@ -69,6 +70,7 @@ export default class Connection { this.userIdsWhoMeMuting = userIdsWhoMeMuting; this.userIdsWhoBlockingMe = userIdsWhoBlockingMe; this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes; + this.userMutedInstances = new Set(userProfile.mutedInstances); } @bindThis @@ -246,6 +248,11 @@ export default class Connection { return; } + if (this.token && ((channelService.kind && !this.token.permission.some(p => p === channelService.kind)) + || (!channelService.kind && channelService.requireCredential))) { + return; + } + // 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視 if (channelService.shouldShare && this.channels.some(c => c.chName === channel)) { return; diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 3aa0d69c0b..80df3803eb 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -16,6 +16,7 @@ export default abstract class Channel { public abstract readonly chName: string; public static readonly shouldShare: boolean; public static readonly requireCredential: boolean; + public static readonly kind?: string | null; protected get user() { return this.connection.user; @@ -41,6 +42,10 @@ export default abstract class Channel { return this.connection.userIdsWhoBlockingMe; } + protected get userMutedInstances() { + return this.connection.userMutedInstances; + } + protected get followingChannels() { return this.connection.followingChannels; } @@ -72,3 +77,10 @@ export default abstract class Channel { public onMessage?(type: string, body: any): void; } + +export type MiChannelService = { + shouldShare: boolean; + requireCredential: T; + kind: T extends true ? string : string | null | undefined; + create: (id: string, connection: Connection) => Channel; +} diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts index bfb36d9cb8..b8f369ce84 100644 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ b/packages/backend/src/server/api/stream/channels/admin.ts @@ -5,12 +5,13 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class AdminChannel extends Channel { public readonly chName = 'admin'; public static shouldShare = true; - public static requireCredential = true; + public static requireCredential = true as const; + public static kind = 'read:admin:stream'; @bindThis public async init(params: any) { @@ -22,9 +23,10 @@ class AdminChannel extends Channel { } @Injectable() -export class AdminChannelService { +export class AdminChannelService implements MiChannelService { public readonly shouldShare = AdminChannel.shouldShare; public readonly requireCredential = AdminChannel.requireCredential; + public readonly kind = AdminChannel.kind; constructor( ) { diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index a48e6ba5c6..200db8eb0e 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -8,12 +8,13 @@ import { isUserRelated } from '@/misc/is-user-related.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class AntennaChannel extends Channel { public readonly chName = 'antenna'; public static shouldShare = false; - public static requireCredential = false; + public static requireCredential = true as const; + public static kind = 'read:account'; private antennaId: string; constructor( @@ -62,9 +63,10 @@ class AntennaChannel extends Channel { } @Injectable() -export class AntennaChannelService { +export class AntennaChannelService implements MiChannelService { public readonly shouldShare = AntennaChannel.shouldShare; public readonly requireCredential = AntennaChannel.requireCredential; + public readonly kind = AntennaChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts new file mode 100644 index 0000000000..4f8809edbe --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts @@ -0,0 +1,125 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { MetaService } from '@/core/MetaService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; +import type { MiMeta } from '@/models/Meta.js'; +import Channel, { MiChannelService } from '../channel.js'; + +class BubbleTimelineChannel extends Channel { + public readonly chName = 'bubbleTimeline'; + public static shouldShare = false; + public static requireCredential = false as const; + private withRenotes: boolean; + private withFiles: boolean; + private withBots: boolean; + private instance: MiMeta; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + //this.onNote = this.onNote.bind(this); + } + + @bindThis + public async init(params: any) { + const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); + if (!policies.btlAvailable) return; + + this.withRenotes = params.withRenotes ?? true; + this.withFiles = params.withFiles ?? false; + this.withBots = params.withBots ?? true; + this.instance = await this.metaService.fetch(); + + // Subscribe events + this.subscriber.on('notesStream', this.onNote); + } + + @bindThis + private async onNote(note: Packed<'Note'>) { + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (!this.withBots && note.user.isBot) return; + + if (!(note.user.host != null && this.instance.bubbleInstances.includes(note.user.host) && note.visibility === 'public' )) return; + + if (note.channelId != null) return; + + // 関係ない返信は除外 + if (note.reply && !this.following[note.userId]?.withReplies) { + const reply = note.reply; + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; + } + + if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return; + + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + + // Ignore notes from instances the user has muted + if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? [])) && !this.following[note.userId]) return; + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoMeMuting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; + + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; + + if (this.user && note.renoteId && !note.text) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } + } + + this.connection.cacheNote(note); + + this.send('note', note); + } + + @bindThis + public dispose() { + // Unsubscribe events + this.subscriber.off('notesStream', this.onNote); + } +} + +@Injectable() +export class BubbleTimelineChannelService implements MiChannelService { + public readonly shouldShare = BubbleTimelineChannel.shouldShare; + public readonly requireCredential = BubbleTimelineChannel.requireCredential; + public readonly kind = BubbleTimelineChannel.kind; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private noteEntityService: NoteEntityService, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): BubbleTimelineChannel { + return new BubbleTimelineChannel( + this.metaService, + this.roleService, + this.noteEntityService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 57034231a3..20275249b8 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -8,12 +8,12 @@ import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class ChannelChannel extends Channel { public readonly chName = 'channel'; public static shouldShare = false; - public static requireCredential = false; + public static requireCredential = false as const; private channelId: string; constructor( @@ -65,9 +65,10 @@ class ChannelChannel extends Channel { } @Injectable() -export class ChannelChannelService { +export class ChannelChannelService implements MiChannelService { public readonly shouldShare = ChannelChannel.shouldShare; public readonly requireCredential = ChannelChannel.requireCredential; + public readonly kind = ChannelChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts index 83f53c1836..4bf34a72c9 100644 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ b/packages/backend/src/server/api/stream/channels/drive.ts @@ -5,12 +5,13 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class DriveChannel extends Channel { public readonly chName = 'drive'; public static shouldShare = true; - public static requireCredential = true; + public static requireCredential = true as const; + public static kind = 'read:account'; @bindThis public async init(params: any) { @@ -22,9 +23,10 @@ class DriveChannel extends Channel { } @Injectable() -export class DriveChannelService { +export class DriveChannelService implements MiChannelService { public readonly shouldShare = DriveChannel.shouldShare; public readonly requireCredential = DriveChannel.requireCredential; + public readonly kind = DriveChannel.kind; constructor( ) { diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index fa0493854b..e05e380aae 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -12,12 +12,12 @@ import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class GlobalTimelineChannel extends Channel { public readonly chName = 'globalTimeline'; public static shouldShare = false; - public static requireCredential = false; + public static requireCredential = false as const; private withRenotes: boolean; private withFiles: boolean; private withBots: boolean; @@ -67,7 +67,7 @@ class GlobalTimelineChannel extends Channel { if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; // Ignore notes from instances the user has muted - if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return; + if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? [])) && !this.following[note.userId]) return; // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; @@ -96,9 +96,10 @@ class GlobalTimelineChannel extends Channel { } @Injectable() -export class GlobalTimelineChannelService { +export class GlobalTimelineChannelService implements MiChannelService { public readonly shouldShare = GlobalTimelineChannel.shouldShare; public readonly requireCredential = GlobalTimelineChannel.requireCredential; + public readonly kind = GlobalTimelineChannel.kind; constructor( private metaService: MetaService, diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index f30b29cfd6..3d4f2fc528 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -9,12 +9,12 @@ import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class HashtagChannel extends Channel { public readonly chName = 'hashtag'; public static shouldShare = false; - public static requireCredential = false; + public static requireCredential = false as const; private q: string[][]; constructor( @@ -70,9 +70,10 @@ class HashtagChannel extends Channel { } @Injectable() -export class HashtagChannelService { +export class HashtagChannelService implements MiChannelService { public readonly shouldShare = HashtagChannel.shouldShare; public readonly requireCredential = HashtagChannel.requireCredential; + public readonly kind = HashtagChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 32bb9fd984..3b499b7bf3 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -10,12 +10,13 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class HomeTimelineChannel extends Channel { public readonly chName = 'homeTimeline'; public static shouldShare = false; - public static requireCredential = true; + public static requireCredential = true as const; + public static kind = 'read:account'; private withRenotes: boolean; private withFiles: boolean; @@ -51,7 +52,7 @@ class HomeTimelineChannel extends Channel { } // Ignore notes from instances the user has muted - if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances))) return; + if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances)) && !this.following[note.userId]) return; if (note.visibility === 'followers') { if (!isMe && !Object.hasOwn(this.following, note.userId)) return; @@ -101,9 +102,10 @@ class HomeTimelineChannel extends Channel { } @Injectable() -export class HomeTimelineChannelService { +export class HomeTimelineChannelService implements MiChannelService { public readonly shouldShare = HomeTimelineChannel.shouldShare; public readonly requireCredential = HomeTimelineChannel.requireCredential; + public readonly kind = HomeTimelineChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index cf904b475a..26cbbebe83 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -12,12 +12,13 @@ import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class HybridTimelineChannel extends Channel { public readonly chName = 'hybridTimeline'; public static shouldShare = false; - public static requireCredential = true; + public static requireCredential = true as const; + public static kind = 'read:account'; private withRenotes: boolean; private withReplies: boolean; private withBots: boolean; @@ -74,7 +75,7 @@ class HybridTimelineChannel extends Channel { } // Ignore notes from instances the user has muted - if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances))) return; + if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances)) && !this.following[note.userId]) return; if (note.reply) { const reply = note.reply; @@ -119,9 +120,10 @@ class HybridTimelineChannel extends Channel { } @Injectable() -export class HybridTimelineChannelService { +export class HybridTimelineChannelService implements MiChannelService { public readonly shouldShare = HybridTimelineChannel.shouldShare; public readonly requireCredential = HybridTimelineChannel.requireCredential; + public readonly kind = HybridTimelineChannel.kind; constructor( private metaService: MetaService, diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 419159197a..40342b6c7b 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -11,12 +11,12 @@ import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class LocalTimelineChannel extends Channel { public readonly chName = 'localTimeline'; public static shouldShare = false; - public static requireCredential = false; + public static requireCredential = false as const; private withRenotes: boolean; private withReplies: boolean; private withBots: boolean; @@ -95,9 +95,10 @@ class LocalTimelineChannel extends Channel { } @Injectable() -export class LocalTimelineChannelService { +export class LocalTimelineChannelService implements MiChannelService { public readonly shouldShare = LocalTimelineChannel.shouldShare; public readonly requireCredential = LocalTimelineChannel.requireCredential; + public readonly kind = LocalTimelineChannel.kind; constructor( private metaService: MetaService, diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index f969d02337..ab605e3ec5 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -7,12 +7,13 @@ import { Injectable } from '@nestjs/common'; import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class MainChannel extends Channel { public readonly chName = 'main'; public static shouldShare = true; - public static requireCredential = true; + public static requireCredential = true as const; + public static kind = 'read:account'; constructor( private noteEntityService: NoteEntityService, @@ -63,9 +64,10 @@ class MainChannel extends Channel { } @Injectable() -export class MainChannelService { +export class MainChannelService implements MiChannelService { public readonly shouldShare = MainChannel.shouldShare; public readonly requireCredential = MainChannel.requireCredential; + public readonly kind = MainChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts index f0dc472303..5ceb2c3bbc 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -6,14 +6,14 @@ import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; const ev = new Xev(); class QueueStatsChannel extends Channel { public readonly chName = 'queueStats'; public static shouldShare = true; - public static requireCredential = false; + public static requireCredential = false as const; constructor(id: string, connection: Channel['connection']) { super(id, connection); @@ -53,9 +53,10 @@ class QueueStatsChannel extends Channel { } @Injectable() -export class QueueStatsChannelService { +export class QueueStatsChannelService implements MiChannelService { public readonly shouldShare = QueueStatsChannel.shouldShare; public readonly requireCredential = QueueStatsChannel.requireCredential; + public readonly kind = QueueStatsChannel.kind; constructor( ) { diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index 38d3604cc5..b3bbb77dbf 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -10,12 +10,12 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; class RoleTimelineChannel extends Channel { public readonly chName = 'roleTimeline'; public static shouldShare = false; - public static requireCredential = false; + public static requireCredential = false as const; private roleId: string; constructor( @@ -67,9 +67,10 @@ class RoleTimelineChannel extends Channel { } @Injectable() -export class RoleTimelineChannelService { +export class RoleTimelineChannelService implements MiChannelService { public readonly shouldShare = RoleTimelineChannel.shouldShare; public readonly requireCredential = RoleTimelineChannel.requireCredential; + public readonly kind = RoleTimelineChannel.kind; constructor( private noteEntityService: NoteEntityService, diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts index cacae275a8..615b6946cc 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -6,14 +6,14 @@ import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; +import Channel, { type MiChannelService } from '../channel.js'; const ev = new Xev(); class ServerStatsChannel extends Channel { public readonly chName = 'serverStats'; public static shouldShare = true; - public static requireCredential = false; + public static requireCredential = false as const; constructor(id: string, connection: Channel['connection']) { super(id, connection); @@ -53,9 +53,10 @@ class ServerStatsChannel extends Channel { } @Injectable() -export class ServerStatsChannelService { +export class ServerStatsChannelService implements MiChannelService { public readonly shouldShare = ServerStatsChannel.shouldShare; public readonly requireCredential = ServerStatsChannel.requireCredential; + public readonly kind = ServerStatsChannel.kind; constructor( ) { diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 4b6628df6f..909b5a5e03 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -5,18 +5,18 @@ import { Inject, Injectable } from '@nestjs/common'; import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; +import Channel, { type MiChannelService } from '../channel.js'; class UserListChannel extends Channel { public readonly chName = 'userList'; public static shouldShare = false; - public static requireCredential = false; + public static requireCredential = false as const; private listId: string; private membershipsMap: Record | undefined> = {}; private listUsersClock: NodeJS.Timeout; @@ -80,6 +80,9 @@ class UserListChannel extends Channel { private async onNote(note: Packed<'Note'>) { const isMe = this.user!.id === note.userId; + // チャンネル投稿は無視する + if (note.channelId) return; + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; if (!Object.hasOwn(this.membershipsMap, note.userId)) return; @@ -115,6 +118,9 @@ class UserListChannel extends Channel { } } + // 流れてきたNoteがミュートしているインスタンスに関わるものだったら無視する + if (isInstanceMuted(note, this.userMutedInstances)) return; + this.connection.cacheNote(note); this.send('note', note); @@ -131,9 +137,10 @@ class UserListChannel extends Channel { } @Injectable() -export class UserListChannelService { +export class UserListChannelService implements MiChannelService { public readonly shouldShare = UserListChannel.shouldShare; public readonly requireCredential = UserListChannel.requireCredential; + public readonly kind = UserListChannel.kind; constructor( @Inject(DI.userListsRepository) diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index 7ccf3a297e..6de9038620 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -16,6 +16,41 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type { FastifyInstance } from 'fastify'; +const kinds = [ + 'read:account', + 'write:account', + 'read:blocks', + 'write:blocks', + 'read:drive', + 'write:drive', + 'read:favorites', + 'write:favorites', + 'read:following', + 'write:following', + 'read:messaging', + 'write:messaging', + 'read:mutes', + 'write:mutes', + 'write:notes', + 'read:notifications', + 'write:notifications', + 'read:reactions', + 'write:reactions', + 'write:votes', + 'read:pages', + 'write:pages', + 'write:page-likes', + 'read:page-likes', + 'read:user-groups', + 'write:user-groups', + 'read:channels', + 'write:channels', + 'read:gallery', + 'write:gallery', + 'read:gallery-likes', + 'write:gallery-likes', +]; + function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface { const accessTokenArr = authorization?.split(' ') ?? [null]; const accessToken = accessTokenArr[accessTokenArr.length - 1]; @@ -31,6 +66,22 @@ export class OAuth2ProviderService { private config: Config, ) { } + // https://datatracker.ietf.org/doc/html/rfc8414.html + // https://indieauth.spec.indieweb.org/#indieauth-server-metadata + public generateRFC8414() { + return { + issuer: this.config.url, + authorization_endpoint: new URL('/oauth/authorize', this.config.url), + token_endpoint: new URL('/oauth/token', this.config.url), + scopes_supported: kinds, + response_types_supported: ['code'], + grant_types_supported: ['authorization_code'], + service_documentation: 'https://misskey-hub.net', + code_challenge_methods_supported: ['S256'], + authorization_response_iss_parameter_supported: true, + }; + } + @bindThis public async createServer(fastify: FastifyInstance): Promise { // https://datatracker.ietf.org/doc/html/rfc8414.html @@ -80,7 +131,7 @@ export class OAuth2ProviderService { fastify.register(multer.contentParser); - fastify.get('/oauth/authorize', async (request, reply) => { + fastify.get('/authorize', async (request, reply) => { const query: any = request.query; let param = "mastodon=true"; if (query.state) param += `&state=${query.state}`; @@ -91,7 +142,7 @@ export class OAuth2ProviderService { ); }); - fastify.get('/oauth/authorize/', async (request, reply) => { + fastify.get('/authorize/', async (request, reply) => { const query: any = request.query; let param = "mastodon=true"; if (query.state) param += `&state=${query.state}`; @@ -102,7 +153,7 @@ export class OAuth2ProviderService { ); }); - fastify.post('/oauth/token', { preHandler: upload.none() }, async (request, reply) => { + fastify.post('/token', { preHandler: upload.none() }, async (request, reply) => { const body: any = request.body || request.query; if (body.grant_type === "client_credentials") { const ret = { diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index ea7bfd70b8..aa696046ea 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -178,6 +178,7 @@ export class ClientServerService { infoImageUrl: meta.infoImageUrl ?? 'https://launcher.moe/nothinghere.png', notFoundImageUrl: meta.notFoundImageUrl ?? 'https://launcher.moe/missingpage.webp', instanceUrl: this.config.url, + randomMOTD: this.config.customMOTD ? this.config.customMOTD[Math.floor(Math.random() * this.config.customMOTD.length)] : undefined, }; } diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index 94d0040313..aaa9566a76 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -58,9 +58,9 @@ export class FeedService { const feed = new Feed({ id: author.link, title: `${author.name} (@${user.username}@${this.config.host})`, - updated: this.idService.parse(notes[0].id).date, + updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined, generator: 'Sharkey', - description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, + description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, link: author.link, image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), feedLinks: { diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index e1864ae124..aac6689e12 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -106,8 +106,29 @@ //#region Theme const theme = localStorage.getItem('theme'); + const themeFontFaceName = 'sharkey-theme-font-face'; if (theme) { - for (const [k, v] of Object.entries(JSON.parse(theme))) { + let existingFontFace; + document.fonts.forEach((v,k,s)=>{if (v.family === themeFontFaceName) existingFontFace=v;}); + if (existingFontFace) document.fonts.delete(existingFontFace); + + const themeProps = JSON.parse(theme); + const fontFaceSrc = themeProps.fontFaceSrc; + const fontFaceOpts = themeProps.fontFaceOpts || {}; + if (fontFaceSrc) { + const fontFace = new FontFace( + themeFontFaceName, + fontFaceSrc, fontFaceOpts || {}, + ); + document.fonts.add(fontFace); + fontFace.load().catch( + (failure) => { + console.log(failure) + } + ); + } + for (const [k, v] of Object.entries(themeProps)) { + if (k.startsWith('font')) continue; document.documentElement.style.setProperty(`--${k}`, v.toString()); // HTMLの theme-color 適用 @@ -178,6 +199,7 @@

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

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

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

+

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

Other options / その他のオプション diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index 952be9bf0b..171827a523 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -44,7 +44,7 @@ html { display: inline-block; width: 28px; height: 28px; - transform: translateY(70px); + transform: translateY(80px); color: var(--accent); } #splashSpinner > .spinner { @@ -74,3 +74,17 @@ html { transform: rotate(360deg); } } + +#splashText { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + display: inline-block; + width: 70%; + height: 0; + text-align: center; + transform: translateY(40px); +} diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 243d18202f..c15e123a15 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -9,7 +9,7 @@ doctype html - _____ _ _ / ___| | | | - \ `--.| |__ __ _ _ __| | _____ _ _ + \ `--.| |__ __ _ _ __| | _____ _ _ `--. \ '_ \ / _` | '__| |/ / _ \ | | | /\__/ / | | | (_| | | | < __/ |_| | \____/|_| |_|\__,_|_| |_|\_\___|\__, | @@ -18,7 +18,7 @@ doctype html Thank you for using Sharkey! If you are reading this message... how about joining the development? - https://github.com/transfem-org/sharkey + https://git.joinsharkey.org/Sharkey/Sharkey html @@ -41,6 +41,7 @@ html link(rel='prefetch' href=notFoundImageUrl) //- https://github.com/misskey-dev/misskey/issues/9842 link(rel='stylesheet' href='/assets/phosphor-icons/bold/style.css') + link(rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) script(src='/client-assets/libopenmpt.js') @@ -84,6 +85,9 @@ html | Please turn on your JavaScript div#splash img#splashIcon(src= icon || '/static-assets/splash.png') + span#splashText + block randomMOTD + = randomMOTD div#splashSpinner diff --git a/packages/backend/src/server/web/views/error.pug b/packages/backend/src/server/web/views/error.pug index 679970a0a5..00a2a72d7a 100644 --- a/packages/backend/src/server/web/views/error.pug +++ b/packages/backend/src/server/web/views/error.pug @@ -4,7 +4,7 @@ doctype html - _____ _ _ / ___| | | | - \ `--.| |__ __ _ _ __| | _____ _ _ + \ `--.| |__ __ _ _ __| | _____ _ _ `--. \ '_ \ / _` | '__| |/ / _ \ | | | /\__/ / | | | (_| | | | < __/ |_| | \____/|_| |_|\__,_|_| |_|\_\___|\__, | @@ -13,8 +13,8 @@ doctype html Thank you for using Sharkey! If you are reading this message... how about joining the development? - https://github.com/transfem-org/sharkey - + https://git.joinsharkey.org/Sharkey/Sharkey + html diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index f88a1d4e56..e55952f296 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -14,18 +14,34 @@ * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された + * roleAssigned - ロールが付与された * achievementEarned - 実績を獲得 * app - アプリ通知 * test - テスト通知(サーバー側) */ -export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const; +export const notificationTypes = [ + 'note', + 'follow', + 'mention', + 'reply', + 'renote', + 'quote', + 'reaction', + 'pollEnded', + 'receiveFollowRequest', + 'followRequestAccepted', + 'roleAssigned', + 'achievementEarned', + 'app', + 'test'] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; -export const ffVisibility = ['public', 'followers', 'private'] as const; +export const followingVisibilities = ['public', 'followers', 'private'] as const; +export const followersVisibilities = ['public', 'followers', 'private'] as const; export const moderationLogTypes = [ 'updateServerSettings', @@ -64,6 +80,8 @@ export const moderationLogTypes = [ 'createAvatarDecoration', 'updateAvatarDecoration', 'deleteAvatarDecoration', + 'unsetUserAvatar', + 'unsetUserBanner', ] as const; export type ModerationLogPayloads = { @@ -243,6 +261,18 @@ export type ModerationLogPayloads = { avatarDecorationId: string; avatarDecoration: any; }; + unsetUserAvatar: { + userId: string; + userUsername: string; + userHost: string | null; + fileId: string; + }; + unsetUserBanner: { + userId: string; + userUsername: string; + userHost: string | null; + fileId: string; + }; }; export type Serialized = { diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/docker-compose.yml index da6c01dda1..f2d8990758 100644 --- a/packages/backend/test/docker-compose.yml +++ b/packages/backend/test/docker-compose.yml @@ -7,7 +7,7 @@ services: - "127.0.0.1:56312:6379" dbtest: - image: postgres:13 + image: postgres:15 ports: - "127.0.0.1:54312:5432" environment: diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index 15da74931d..cf24228b83 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -7,7 +7,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { IncomingMessage } from 'http'; -import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch } from '../utils.js'; +import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; @@ -89,6 +89,11 @@ describe('API', () => { }); test('管理者専用のAPIのアクセス制限', async () => { + const application = await createAppToken(alice, ['read:account']); + const application2 = await createAppToken(alice, ['read:admin:index-stats']); + const application3 = await createAppToken(bob, []); + const application4 = await createAppToken(bob, ['read:admin:index-stats']); + // aliceは管理者、APIを使える await successfulApiCall({ endpoint: '/admin/get-index-stats', @@ -128,6 +133,42 @@ describe('API', () => { code: 'AUTHENTICATION_FAILED', id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', }); + + await successfulApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: { token: application2 }, + }); + + await failedApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: { token: application }, + }, { + status: 403, + code: 'PERMISSION_DENIED', + id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', + }); + + await failedApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: { token: application3 }, + }, { + status: 403, + code: 'ROLE_PERMISSION_DENIED', + id: 'c3d38592-54c0-429d-be96-5636b0431a61', + }); + + await failedApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: { token: application4 }, + }, { + status: 403, + code: 'ROLE_PERMISSION_DENIED', + id: 'c3d38592-54c0-429d-be96-5636b0431a61', + }); }); describe('Authentication header', () => { diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 1cbfec3e5f..251d662760 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -93,7 +93,7 @@ describe('Webリソース', () => { }); aliceChannel = await channel(alice, {}); - bob = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); afterAll(async () => { @@ -152,6 +152,11 @@ describe('Webリソース', () => { type, })); + test('がGETできる。(ノートが存在しない場合でも。)', async () => await ok({ + path: path(bob.username), + type, + })); + test('は存在しないユーザーはGETできない。', async () => await notOk({ path: path('nonexisting'), status: 404, diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 7841e057bf..1fbd45c741 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -26,9 +26,10 @@ describe('FF visibility', () => { await app.close(); }); - test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { + test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { await api('/i/update', { - ffVisibility: 'public', + followingVisibility: 'public', + followersVisibility: 'public', }, alice); const followingRes = await api('/users/following', { @@ -44,9 +45,88 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { + test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'public', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'public', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', + followersVisibility: 'followers', }, alice); const followingRes = await api('/users/following', { @@ -62,9 +142,88 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { + test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', + followersVisibility: 'followers', }, alice); const followingRes = await api('/users/following', { @@ -78,9 +237,82 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 400); }); - test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { + test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + }); + + test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + }); + + test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', + followersVisibility: 'followers', }, alice); await api('/following/create', { @@ -100,9 +332,106 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => { + test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => { await api('/i/update', { - ffVisibility: 'private', + followingVisibility: 'private', + followersVisibility: 'private', }, alice); const followingRes = await api('/users/following', { @@ -118,9 +447,88 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => { + test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => { await api('/i/update', { - ffVisibility: 'private', + followingVisibility: 'private', + followersVisibility: 'private', }, alice); const followingRes = await api('/users/following', { @@ -134,36 +542,129 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 400); }); + test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + }); + + test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + }); + describe('AP', () => { - test('ffVisibility が public 以外ならばAPからは取得できない', async () => { + test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => { { await api('/i/update', { - ffVisibility: 'public', + followingVisibility: 'public', }, alice); const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followingRes.status, 200); + } + { + await api('/i/update', { + followingVisibility: 'followers', + }, alice); + + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 403); + } + { + await api('/i/update', { + followingVisibility: 'private', + }, alice); + + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 403); + } + }); + + test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => { + { + await api('/i/update', { + followersVisibility: 'public', + }, alice); + + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followersRes.status, 200); } { await api('/i/update', { - ffVisibility: 'followers', + followersVisibility: 'followers', }, alice); - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); - assert.strictEqual(followingRes.status, 403); assert.strictEqual(followersRes.status, 403); } { await api('/i/update', { - ffVisibility: 'private', + followersVisibility: 'private', }, alice); - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); - assert.strictEqual(followingRes.status, 403); assert.strictEqual(followersRes.status, 403); } }); diff --git a/packages/backend/test/e2e/nodeinfo.ts b/packages/backend/test/e2e/nodeinfo.ts new file mode 100644 index 0000000000..7eed39c5ed --- /dev/null +++ b/packages/backend/test/e2e/nodeinfo.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { relativeFetch, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('nodeinfo', () => { + let app: INestApplicationContext; + + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + test('nodeinfo 2.1', async () => { + const res = await relativeFetch('nodeinfo/2.1'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const nodeInfo = await res.json() as any; + assert.strictEqual(nodeInfo.software.name, 'misskey'); + }); + + test('nodeinfo 2.0', async () => { + const res = await relativeFetch('nodeinfo/2.0'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const nodeInfo = await res.json() as any; + assert.strictEqual(nodeInfo.software.name, 'misskey'); + }); +}); diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index a029a0d4be..3a5e4ebdae 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -941,4 +941,24 @@ describe('OAuth', () => { const response = await fetch(new URL('/oauth/foo', host)); assert.strictEqual(response.status, 404); }); + + describe('CORS', () => { + test('Token endpoint should support CORS', async () => { + const response = await fetch(new URL('/oauth/token', host), { method: 'POST' }); + assert.ok(!response.ok); + assert.strictEqual(response.headers.get('Access-Control-Allow-Origin'), '*'); + }); + + test('Authorize endpoint should not support CORS', async () => { + const response = await fetch(new URL('/oauth/authorize', host), { method: 'GET' }); + assert.ok(!response.ok); + assert.ok(!response.headers.has('Access-Control-Allow-Origin')); + }); + + test('Decision endpoint should not support CORS', async () => { + const response = await fetch(new URL('/oauth/decision', host), { method: 'POST' }); + assert.ok(!response.ok); + assert.ok(!response.headers.has('Access-Control-Allow-Origin')); + }); + }); }); diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index f9f385e2b2..288c54bdbc 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -6,8 +6,9 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { WebSocket } from 'ws'; import { MiFollowing } from '@/models/Following.js'; -import { connectStream, signup, api, post, startServer, initTestDb, waitFire } from '../utils.js'; +import { signup, api, post, startServer, initTestDb, waitFire, createAppToken, port } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; @@ -34,12 +35,16 @@ describe('Streaming', () => { let ayano: misskey.entities.MeSignup; let kyoko: misskey.entities.MeSignup; let chitose: misskey.entities.MeSignup; + let kanako: misskey.entities.MeSignup; // Remote users let akari: misskey.entities.MeSignup; let chinatsu: misskey.entities.MeSignup; + let takumi: misskey.entities.MeSignup; let kyokoNote: any; + let kanakoNote: any; + let takumiNote: any; let list: any; beforeAll(async () => { @@ -50,11 +55,15 @@ describe('Streaming', () => { ayano = await signup({ username: 'ayano' }); kyoko = await signup({ username: 'kyoko' }); chitose = await signup({ username: 'chitose' }); + kanako = await signup({ username: 'kanako' }); akari = await signup({ username: 'akari', host: 'example.com' }); chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); + takumi = await signup({ username: 'takumi', host: 'example.com' }); kyokoNote = await post(kyoko, { text: 'foo' }); + kanakoNote = await post(kanako, { text: 'hoge' }); + takumiNote = await post(takumi, { text: 'piyo' }); // Follow: ayano => kyoko await api('following/create', { userId: kyoko.id }, ayano); @@ -62,6 +71,9 @@ describe('Streaming', () => { // Follow: ayano => akari await follow(ayano, akari); + // Mute: chitose => kanako + await api('mute/create', { userId: kanako.id }, chitose); + // List: chitose => ayano, kyoko list = await api('users/lists/create', { name: 'my list', @@ -76,6 +88,11 @@ describe('Streaming', () => { listId: list.id, userId: kyoko.id, }, chitose); + + await api('users/lists/push', { + listId: list.id, + userId: takumi.id, + }, chitose); }, 1000 * 60 * 2); afterAll(async () => { @@ -452,6 +469,118 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); + + // #10443 + test('チャンネル投稿は流れない', async () => { + // リスインしている kyoko が 任意のチャンネルに投降した時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', channelId: 'dummy' }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているユーザへのリプライがリストTLに流れない', async () => { + // chitose が kanako をミュートしている状態で、リスインしている kyoko が kanako にリプライした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', replyId: kanakoNote.id }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているユーザの投稿をリノートしたときリストTLに流れない', async () => { + // chitose が kanako をミュートしている状態で、リスインしている kyoko が kanako のノートをリノートした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { renoteId: kanakoNote.id }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているサーバのノートがリストTLに流れない', async () => { + await api('/i/update', { + mutedInstances: ['example.com'], + }, chitose); + + // chitose が example.com をミュートしている状態で、リスインしている takumi が ノートした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo' }, takumi), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているサーバのノートに対するリプライがリストTLに流れない', async () => { + await api('/i/update', { + mutedInstances: ['example.com'], + }, chitose); + + // chitose が example.com をミュートしている状態で、リスインしている kyoko が takumi のノートにリプライした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', replyId: takumiNote.id }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #10443 + test('ミュートしているサーバのノートに対するリノートがリストTLに流れない', async () => { + await api('/i/update', { + mutedInstances: ['example.com'], + }, chitose); + + // chitose が example.com をミュートしている状態で、リスインしている kyoko が takumi のノートをリノートした時の動きを見たい + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { renoteId: takumiNote.id }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + }); + + test('Authentication', async () => { + const application = await createAppToken(ayano, []); + const application2 = await createAppToken(ayano, ['read:account']); + const socket = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${application}`); + const established = await new Promise((resolve, reject) => { + socket.on('error', () => resolve(false)); + socket.on('unexpected-response', () => resolve(false)); + setTimeout(() => resolve(true), 3000); + }); + + socket.close(); + assert.strictEqual(established, false); + + const fired = await waitFire( + { token: application2 }, 'hybridTimeline', + () => api('notes/create', { text: 'Hello, world!' }, ayano), + msg => msg.type === 'note' && msg.body.userId === ayano.id, + ); + + assert.strictEqual(fired, true); }); // XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac" diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 73c446444b..cb9558b416 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -10,9 +10,8 @@ process.env.NODE_ENV = 'test'; process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true'; import * as assert from 'assert'; -import { signup, api, post, react, startServer, waitFire, sleep, uploadUrl, randomString } from '../utils.js'; +import { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; -import type * as misskey from 'misskey-js'; function genHost() { return randomString() + '.example.com'; @@ -366,8 +365,8 @@ describe('Timelines', () => { await api('/following/create', { userId: bob.id }, alice); await sleep(1000); const [bobFile, carolFile] = await Promise.all([ - uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), - uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), + uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), + uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), ]); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [bobFile.id] }); @@ -666,7 +665,7 @@ describe('Timelines', () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); @@ -804,7 +803,7 @@ describe('Timelines', () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); @@ -999,7 +998,7 @@ describe('Timelines', () => { const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); @@ -1158,7 +1157,7 @@ describe('Timelines', () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 67b2a36ec2..be6f0ec855 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -115,7 +115,8 @@ describe('ユーザー', () => { pinnedPageId: user.pinnedPageId, pinnedPage: user.pinnedPage, publicReactions: user.publicReactions, - ffVisibility: user.ffVisibility, + followingVisibility: user.followingVisibility, + followersVisibility: user.followersVisibility, twoFactorEnabled: user.twoFactorEnabled, usePasswordLessLogin: user.usePasswordLessLogin, securityKeys: user.securityKeys, @@ -171,6 +172,7 @@ describe('ユーザー', () => { hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, unreadAnnouncements: user.unreadAnnouncements, mutedWords: user.mutedWords, + hardMutedWords: user.hardMutedWords, mutedInstances: user.mutedInstances, mutingNotificationTypes: user.mutingNotificationTypes, notificationRecieveConfig: user.notificationRecieveConfig, @@ -391,7 +393,8 @@ describe('ユーザー', () => { assert.strictEqual(response.pinnedPageId, null); assert.strictEqual(response.pinnedPage, null); assert.strictEqual(response.publicReactions, true); - assert.strictEqual(response.ffVisibility, 'public'); + assert.strictEqual(response.followingVisibility, 'public'); + assert.strictEqual(response.followersVisibility, 'public'); assert.strictEqual(response.twoFactorEnabled, false); assert.strictEqual(response.usePasswordLessLogin, false); assert.strictEqual(response.securityKeys, false); @@ -502,9 +505,12 @@ describe('ユーザー', () => { { parameters: (): object => ({ alwaysMarkNsfw: false }) }, { parameters: (): object => ({ autoSensitive: true }) }, { parameters: (): object => ({ autoSensitive: false }) }, - { parameters: (): object => ({ ffVisibility: 'private' }) }, - { parameters: (): object => ({ ffVisibility: 'followers' }) }, - { parameters: (): object => ({ ffVisibility: 'public' }) }, + { parameters: (): object => ({ followingVisibility: 'private' }) }, + { parameters: (): object => ({ followingVisibility: 'followers' }) }, + { parameters: (): object => ({ followingVisibility: 'public' }) }, + { parameters: (): object => ({ followersVisibility: 'private' }) }, + { parameters: (): object => ({ followersVisibility: 'followers' }) }, + { parameters: (): object => ({ followersVisibility: 'public' }) }, { parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, { parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, { parameters: (): object => ({ mutedWords: [] }) }, diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts new file mode 100644 index 0000000000..14e32e1627 --- /dev/null +++ b/packages/backend/test/e2e/well-known.ts @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { host, origin, relativeFetch, signup, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; + +describe('.well-known', () => { + let app: INestApplicationContext; + let alice: misskey.entities.User; + + beforeAll(async () => { + app = await startServer(); + + alice = await signup({ username: 'alice' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + test('nodeinfo', async () => { + const res = await relativeFetch('.well-known/nodeinfo'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const nodeInfo = await res.json(); + assert.deepStrictEqual(nodeInfo, { + links: [{ + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', + href: `${origin}/nodeinfo/2.1`, + }, { + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', + href: `${origin}/nodeinfo/2.0`, + }], + }); + }); + + test('webfinger', async () => { + const preflight = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`, { + method: 'options', + headers: { + 'Access-Control-Request-Method': 'GET', + Origin: 'http://example.com', + }, + }); + assert.ok(preflight.ok); + assert.strictEqual(preflight.headers.get('Access-Control-Allow-Headers'), 'Accept'); + + const res = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + assert.strictEqual(res.headers.get('Access-Control-Expose-Headers'), 'Vary'); + assert.strictEqual(res.headers.get('Vary'), 'Accept'); + + const webfinger = await res.json(); + + assert.deepStrictEqual(webfinger, { + subject: `acct:alice@${host}`, + links: [{ + rel: 'self', + type: 'application/activity+json', + href: `${origin}/users/${alice.id}`, + }, { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/html', + href: `${origin}/@alice`, + }, { + rel: 'http://ostatus.org/schema/1.0/subscribe', + template: `${origin}/authorize-follow?acct={uri}`, + }], + }); + }); + + test('host-meta', async () => { + const res = await relativeFetch('.well-known/host-meta'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + }); + + test('host-meta.json', async () => { + const res = await relativeFetch('.well-known/host-meta.json'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const hostMeta = await res.json(); + assert.deepStrictEqual(hostMeta, { + links: [{ + rel: 'lrdd', + type: 'application/jrd+json', + template: `${origin}/.well-known/webfinger?resource={uri}`, + }], + }); + }); + + test('oauth-authorization-server', async () => { + const res = await relativeFetch('.well-known/oauth-authorization-server'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const serverInfo = await res.json() as any; + assert.strictEqual(serverInfo.issuer, origin); + assert.strictEqual(serverInfo.authorization_endpoint, `${origin}/oauth/authorize`); + assert.strictEqual(serverInfo.token_endpoint, `${origin}/oauth/token`); + }); +}); diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index bb8e6981d5..49e84ccec8 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -4,7 +4,7 @@ */ import * as assert from 'assert'; -import * as mfm from 'mfm-js'; +import * as mfm from '@sharkey/sfm-js'; import { Test } from '@nestjs/testing'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index f644312bc9..9879eb8e3e 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -19,6 +19,7 @@ import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { sleep } from '../utils.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -32,6 +33,7 @@ describe('RoleService', () => { let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; let metaService: jest.Mocked; + let notificationService: jest.Mocked; let clock: lolex.InstalledClock; function createUser(data: Partial = {}) { @@ -71,6 +73,16 @@ describe('RoleService', () => { CacheService, IdService, GlobalEventService, + { + provide: NotificationService, + useFactory: () => ({ + createNotification: jest.fn(), + }), + }, + { + provide: NotificationService.name, + useExisting: NotificationService, + }, ], }) .useMocker((token) => { @@ -93,6 +105,9 @@ describe('RoleService', () => { roleAssignmentsRepository = app.get(DI.roleAssignmentsRepository); metaService = app.get(MetaService) as jest.Mocked; + notificationService = app.get(NotificationService) as jest.Mocked; + + await roleService.onModuleInit(); }); afterEach(async () => { @@ -273,4 +288,57 @@ describe('RoleService', () => { expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); }); }); + + describe('assign', () => { + test('公開ロールの場合は通知される', async () => { + const user = await createUser(); + const role = await createRole({ + isPublic: true, + name: 'a', + }); + + await roleService.assign(user.id, role.id); + + clock.uninstall(); + await sleep(100); + + const assignments = await roleAssignmentsRepository.find({ + where: { + userId: user.id, + roleId: role.id, + }, + }); + expect(assignments).toHaveLength(1); + + expect(notificationService.createNotification).toHaveBeenCalled(); + expect(notificationService.createNotification.mock.lastCall![0]).toBe(user.id); + expect(notificationService.createNotification.mock.lastCall![1]).toBe('roleAssigned'); + expect(notificationService.createNotification.mock.lastCall![2]).toEqual({ + roleId: role.id, + }); + }); + + test('非公開ロールの場合は通知されない', async () => { + const user = await createUser(); + const role = await createRole({ + isPublic: false, + name: 'a', + }); + + await roleService.assign(user.id, role.id); + + clock.uninstall(); + await sleep(100); + + const assignments = await roleAssignmentsRepository.find({ + where: { + userId: user.id, + roleId: role.id, + }, + }); + expect(assignments).toHaveLength(1); + + expect(notificationService.createNotification).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/backend/test/unit/extract-mentions.ts b/packages/backend/test/unit/extract-mentions.ts index 5901f33fdc..195e9b8198 100644 --- a/packages/backend/test/unit/extract-mentions.ts +++ b/packages/backend/test/unit/extract-mentions.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; -import { parse } from 'mfm-js'; +import { parse } from '@sharkey/sfm-js'; import { extractMentions } from '@/misc/extract-mentions.js'; describe('Extract mentions', () => { diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 97118d73c0..46b8ea9cdd 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -6,6 +6,7 @@ import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { isAbsolute, basename } from 'node:path'; +import { randomUUID } from 'node:crypto'; import { inspect } from 'node:util'; import WebSocket, { ClientOptions } from 'ws'; import fetch, { File, RequestInit } from 'node-fetch'; @@ -25,6 +26,8 @@ interface UserToken { const config = loadConfig(); export const port = config.port; +export const origin = config.url; +export const host = new URL(config.url).host; export const cookie = (me: UserToken): string => { return `token=${me.token};`; @@ -126,6 +129,15 @@ export const post = async (user: UserToken, params?: misskey.Endpoints['notes/cr return res.body ? res.body.createdNote : null; }; +export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => { + const res = await api('miauth/gen-token', { + session: randomUUID(), + permission: permissions, + }, user); + + return (res.body as misskey.entities.MiauthGenTokenResponse).token; +}; + // 非公開ノートをAPI越しに見たときのノート NoteEntityService.ts export const hiddenNote = (note: any): any => { const temp = { diff --git a/packages/frontend/.eslintrc.cjs b/packages/frontend/.eslintrc.cjs index 77038f0dfa..20f88dc078 100644 --- a/packages/frontend/.eslintrc.cjs +++ b/packages/frontend/.eslintrc.cjs @@ -69,12 +69,6 @@ module.exports = { 'require': false, '__dirname': false, - // Vue - '$$': false, - '$ref': false, - '$shallowRef': false, - '$computed': false, - // Misskey '_DEV_': false, '_LANGS_': false, diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 71114d5838..fdf98665d0 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -82,7 +82,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi birthday: '2014-06-20', createdAt: '2016-12-28T22:49:51.000Z', description: 'I am a cool user!', - ffVisibility: 'public', + followingVisibility: 'public', + followersVisibility: 'public', roles: [], fields: [ { diff --git a/packages/frontend/.storybook/mocks.ts b/packages/frontend/.storybook/mocks.ts index b60755feea..80e5157c5a 100644 --- a/packages/frontend/.storybook/mocks.ts +++ b/packages/frontend/.storybook/mocks.ts @@ -25,7 +25,7 @@ export const commonHandlers = [ }), rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => { const { codepoints } = req.params; - const value = await fetch(`https://unpkg.com/@discordapp/twemoji@14.1.2/dist/svg/${codepoints}.svg`).then((response) => response.blob()); + const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob()); return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value)); }), ]; diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index ad2cf18a35..8f32c6e622 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -28,6 +28,8 @@ const keys = [ 'd-cherry', 'd-ice', 'd-u0', + 'rosepine', + 'rosepine-dawn', ] await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index 36ff34b48a..30f3ebfb64 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -1,6 +1,6 @@ - +
-
{{ i18n.ts.reporter }}:
+
{{ i18n.ts.reporter }}: @{{ report.reporter.username }}
{{ i18n.ts.moderator }}: @@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index 14e59acad2..40bca11e64 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -26,7 +26,7 @@ export type Captcha = { getResponse(id: string): string; }; -type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile'; +export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile'; type CaptchaContainer = { readonly [_ in CaptchaProvider]?: Captcha; diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue index 9c6e2f00bd..f870b0eef1 100644 --- a/packages/frontend/src/components/MkChannelPreview.vue +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -4,49 +4,70 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index 5434042684..0ec69a69af 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -4,30 +4,38 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts index 29e27e1373..035b727a35 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts +++ b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts @@ -7,7 +7,7 @@ import { expect } from '@storybook/jest'; import { userEvent, waitFor, within } from '@storybook/testing-library'; import { StoryObj } from '@storybook/vue3'; -import { galleryPost } from '../../.storybook/fakes'; +import { galleryPost } from '../../.storybook/fakes.js'; import MkGalleryPostPreview from './MkGalleryPostPreview.vue'; export const Default = { render(args) { diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue index 185a49b5a9..c0b20507fc 100644 --- a/packages/frontend/src/components/MkGoogle.vue +++ b/packages/frontend/src/components/MkGoogle.vue @@ -23,7 +23,7 @@ const query = ref(props.q); const search = () => { const sp = new URLSearchParams(); sp.append('q', query.value); - window.open(`https://www.google.com/search?${sp.toString()}`, '_blank'); + window.open(`https://www.google.com/search?${sp.toString()}`, '_blank', 'noopener'); }; diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index 0022531e58..a57e6c9292 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -101,6 +101,8 @@ function close() { vertical-align: bottom; height: 100px; border-radius: var(--radius); + padding: 10px; + box-sizing: border-box; &:hover { color: var(--accent); diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 114b9b4faf..bda683002d 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkMarquee.vue b/packages/frontend/src/components/MkMarquee.vue index f9d0573227..145b60c8e7 100644 --- a/packages/frontend/src/components/MkMarquee.vue +++ b/packages/frontend/src/components/MkMarquee.vue @@ -27,7 +27,7 @@ export default { }, }, setup(props) { - const contentEl = ref(); + const contentEl = ref(); function calc() { const eachLength = contentEl.value.offsetWidth / props.repeat; diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 42a709ae26..7b0387cefe 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue index 421c0a8af8..c2384423fd 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index 5db2cc100a..a734f93ec9 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._initialTutorial._done.title }}
{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index 699d7af33e..07efaf8982 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -27,7 +27,7 @@ const modal = shallowRef>(); const whatIsNew = () => { modal.value.close(); - window.open(`https://misskey-hub.net/docs/releases.html#_${version.replace(/\./g, '-')}`, '_blank'); + window.open(`https://git.joinsharkey.org/Sharkey/Sharkey/releases/tag/${version}`, '_blank'); }; onMounted(() => { diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 78c62e1250..486aaa0bbd 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only