diff --git a/.config/example.yml b/.config/example.yml index d9f6238219..ea5d1094a5 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -131,11 +131,20 @@ proxyBypassHosts: # Media Proxy # Reference Implementation: https://github.com/misskey-dev/media-proxy +# * Deliver a common cache between instances +# * Perform image compression (on a different server resource than the main process) #mediaProxy: https://example.com/proxy # Proxy remote files (default: false) +# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. #proxyRemoteFiles: true +# Movie Thumbnail Generation URL +# There is no reference implementation. +# For example, Misskey will point to the following URL: +# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 +#videoThumbnailGenerator: https://example.com + # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..b6ebcf6ad3 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1 @@ +FROM mcr.microsoft.com/devcontainers/javascript-node:0-18 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..fde7ec0f2b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "name": "Misskey", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + "features": { + "ghcr.io/devcontainers-contrib/features/pnpm:2": {} + }, + "forwardPorts": [3000], + "postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh" +} diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml new file mode 100644 index 0000000000..8a363a15dc --- /dev/null +++ b/.devcontainer/devcontainer.yml @@ -0,0 +1,146 @@ +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Misskey configuration +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +# ┌─────┐ +#───┘ URL └───────────────────────────────────────────────────── + +# Final accessible URL seen by a user. +url: http://127.0.0.1:3000/ + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# URL SETTINGS AFTER THAT! + +# ┌───────────────────────┐ +#───┘ Port and TLS settings └─────────────────────────────────── + +# +# Misskey requires a reverse proxy to support HTTPS connections. +# +# +----- https://example.tld/ ------------+ +# +------+ |+-------------+ +----------------+| +# | User | ---> || Proxy (443) | ---> | Misskey (3000) || +# +------+ |+-------------+ +----------------+| +# +---------------------------------------+ +# +# You need to set up a reverse proxy. (e.g. nginx) +# An encrypted connection with HTTPS is highly recommended +# because tokens may be transferred in GET requests. + +# The port that your Misskey server should listen on. +port: 3000 + +# ┌──────────────────────────┐ +#───┘ PostgreSQL configuration └──────────────────────────────── + +db: + host: db + port: 5432 + + # Database name + db: misskey + + # Auth + user: postgres + pass: postgres + + # Whether disable Caching queries + #disableCache: true + + # Extra Connection options + #extra: + # ssl: true + +# ┌─────────────────────┐ +#───┘ Redis configuration └───────────────────────────────────── + +redis: + host: redis + port: 6379 + #family: 0 # 0=Both, 4=IPv4, 6=IPv6 + #pass: example-pass + #prefix: example-prefix + #db: 1 + +# ┌─────────────────────────────┐ +#───┘ Elasticsearch configuration └───────────────────────────── + +#elasticsearch: +# host: localhost +# port: 9200 +# ssl: false +# user: +# pass: + +# ┌───────────────┐ +#───┘ ID generation └─────────────────────────────────────────── + +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. + +# Available methods: +# aid ... Short, Millisecond accuracy +# meid ... Similar to ObjectID, Millisecond accuracy +# ulid ... Millisecond accuracy +# objectid ... This is left for backward compatibility + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# ID SETTINGS AFTER THAT! + +id: 'aid' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + +# Whether disable HSTS +#disableHsts: true + +# Number of worker processes +#clusterLimit: 1 + +# Job concurrency per worker +# deliverJobConcurrency: 128 +# inboxJobConcurrency: 16 + +# Job rate limiter +# deliverJobPerSec: 128 +# inboxJobPerSec: 16 + +# Job attempts +# deliverJobMaxAttempts: 12 +# inboxJobMaxAttempts: 8 + +# IP address family used for outgoing request (ipv4, ipv6 or dual) +#outgoingAddressFamily: ipv4 + +# Proxy for HTTP/HTTPS +#proxy: http://127.0.0.1:3128 + +proxyBypassHosts: + - api.deepl.com + - api-free.deepl.com + - www.recaptcha.net + - hcaptcha.com + - challenges.cloudflare.com + +# Proxy for SMTP/SMTPS +#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT +#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 +#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 + +# Media Proxy +#mediaProxy: https://example.com/proxy + +# Proxy remote files (default: false) +#proxyRemoteFiles: true + +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + +allowedPrivateNetworks: [ + '127.0.0.1/32' +] + +# Upload or download file size limits (bytes) +#maxFileSize: 262144000 diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..6ec3c86a4a --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + + volumes: + - ../:/workspace:cached + + command: sleep infinity + + networks: + - internal_network + - external_network + + redis: + restart: always + image: redis:7-alpine + networks: + - internal_network + volumes: + - redis-data:/data + healthcheck: + test: "redis-cli ping" + interval: 5s + retries: 20 + + db: + restart: unless-stopped + image: postgres:15-alpine + networks: + - internal_network + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: misskey + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" + interval: 5s + retries: 20 + +volumes: + postgres-data: + redis-data: + +networks: + internal_network: + internal: true + external_network: diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh new file mode 100755 index 0000000000..450c3920c3 --- /dev/null +++ b/.devcontainer/init.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -xe + +sudo chown -R node /workspace +git submodule update --init +pnpm install --frozen-lockfile +cp .devcontainer/devcontainer.yml .config/default.yml +pnpm build +pnpm migrate diff --git a/.editorconfig b/.editorconfig index edccf3a9d5..a6f988f8d7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,7 @@ indent_style = tab indent_size = 2 charset = utf-8 insert_final_newline = true +end_of_line = lf -[*.yml] +[*.{yml,yaml}] indent_style = space diff --git a/.gitattributes b/.gitattributes index a175917f31..246ecb0a60 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ *.glb -diff -text *.blend -diff -text *.afdesign -diff -text +* text=auto eol=lf diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE/01_bug.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE/01_bug.md diff --git a/.github/PULL_REQUEST_TEMPLATE/02_enhance.md b/.github/PULL_REQUEST_TEMPLATE/02_enhance.md new file mode 100644 index 0000000000..79ca97dfa0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/02_enhance.md @@ -0,0 +1,17 @@ + + +# What + + + +# Why + + + +# Additional info (optional) + + diff --git a/.github/PULL_REQUEST_TEMPLATE/03_release.md b/.github/PULL_REQUEST_TEMPLATE/03_release.md new file mode 100644 index 0000000000..b5b832e1dc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/03_release.md @@ -0,0 +1,20 @@ +## 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/reviewer-lottery.yml b/.github/reviewer-lottery.yml new file mode 100644 index 0000000000..fd2fb1913f --- /dev/null +++ b/.github/reviewer-lottery.yml @@ -0,0 +1,10 @@ +groups: + - name: devs + reviewers: 2 + internal_reviewers: 1 + usernames: + - syuilo + - acid-chicken + - EbiseLutica + - rinsuki + - tamaina diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml index 99799672f2..8daea44a83 100644 --- a/.github/workflows/check_copyright_year.yml +++ b/.github/workflows/check_copyright_year.yml @@ -1,18 +1,18 @@ -name: Check copyright year - -on: - push: - branches: - - master - - develop - -jobs: - check_copyright_year: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3.2.0 - - 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 +name: Check copyright year + +on: + push: + branches: + - master + - develop + +jobs: + check_copyright_year: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3.2.0 + - 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/docker-develop.yml b/.github/workflows/docker-develop.yml index a999dc51e6..09a2c33e0c 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -15,7 +15,10 @@ jobs: - name: Check out the repo uses: actions/checkout@v3.3.0 - name: Set up Docker Buildx + id: buildx uses: docker/setup-buildx-action@v2.3.0 + with: + platforms: linux/amd64,linux/arm64 - name: Docker meta id: meta uses: docker/metadata-action@v4 @@ -27,10 +30,13 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: + builder: ${{ steps.buildx.outputs.name }} context: . push: true + platforms: ${{ steps.buildx.outputs.platforms }} + provenance: false tags: misskey/misskey:develop labels: develop cache-from: type=gha diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d7803ce3ec..a465d92eaf 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -13,6 +13,11 @@ jobs: steps: - name: Check out the repo uses: actions/checkout@v3.3.0 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2.3.0 + with: + platforms: linux/amd64,linux/arm64 - name: Docker meta id: meta uses: docker/metadata-action@v4 @@ -31,9 +36,14 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 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 }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b88b97ab0c..d65076ebb2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,54 +1,79 @@ -name: Lint - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - pnpm_install: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3.3.0 - with: - fetch-depth: 0 - submodules: true - - uses: pnpm/action-setup@v2 - with: - version: 7 - run_install: false - - uses: actions/setup-node@v3.6.0 - with: - node-version: 18.x - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - lint: - needs: [pnpm_install] - runs-on: ubuntu-latest - continue-on-error: true - strategy: - matrix: - workspace: - - backend - - frontend - - sw - steps: - - uses: actions/checkout@v3.3.0 - with: - fetch-depth: 0 - submodules: true - - uses: pnpm/action-setup@v2 - with: - version: 7 - run_install: false - - uses: actions/setup-node@v3.6.0 - with: - node-version: 18.x - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - run: pnpm --filter ${{ matrix.workspace }} run lint +name: Lint + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + pnpm_install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3.3.0 + with: + fetch-depth: 0 + submodules: true + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v3.6.0 + with: + node-version: 18.x + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + + lint: + needs: [pnpm_install] + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + workspace: + - backend + - frontend + - sw + steps: + - uses: actions/checkout@v3.3.0 + with: + fetch-depth: 0 + submodules: true + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v3.6.0 + with: + node-version: 18.x + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - run: pnpm --filter ${{ matrix.workspace }} run eslint + + typecheck: + needs: [pnpm_install] + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + workspace: + - backend + steps: + - uses: actions/checkout@v3.3.0 + with: + fetch-depth: 0 + submodules: true + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v3.6.0 + with: + node-version: 18.x + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - run: pnpm --filter ${{ matrix.workspace }} run typecheck diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml index 49f1ba8a34..8adfad9dab 100644 --- a/.github/workflows/pr-preview-destroy.yml +++ b/.github/workflows/pr-preview-destroy.yml @@ -9,14 +9,46 @@ name: Destroy preview environment jobs: destroy-preview-environment: runs-on: ubuntu-latest - if: github.repository == github.event.pull_request.head.repo.full_name steps: + - uses: actions/github-script@v6.3.3 + 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/reviewer_lottery.yml b/.github/workflows/reviewer_lottery.yml new file mode 100644 index 0000000000..33228d7465 --- /dev/null +++ b/.github/workflows/reviewer_lottery.yml @@ -0,0 +1,13 @@ +name: "Reviewer lottery" +on: + pull_request_target: + types: [opened, ready_for_review, reopened] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: uesteibar/reviewer-lottery@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index f532cdaa7e..62b818c629 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ coverage !/.config/docker_example.yml !/.config/docker_example.env docker-compose.yml +!/.devcontainer/docker-compose.yml # misskey /build diff --git a/.vscode/settings.json b/.vscode/settings.json index b7e7b20c17..6a0497946d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,9 @@ { "search.exclude": { "**/node_modules": true + }, + "typescript.tsdk": "node_modules/typescript/lib", + "files.associations": { + "*.test.ts": "typescript" } } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c9bda35348..37a5ab3e1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,168 @@ ## 13.x.x (unreleased) ### Improvements +- ### Bugfixes - You should also include the user name that made the change. --> + +## 13.9.1 (2023/03/03) + +### Bugfixes +- ノートに添付したファイルが表示されない場合があるのを修正 + +## 13.9.0 (2023/03/03) + +### Improvements +- 時限ロール +- アンテナでCWも検索対象にするように +- ノートの操作部をホバー時のみ表示するオプションを追加 +- サウンドを追加 +- サーバーのパフォーマンスを改善 + +### Bugfixes +- 外部メディアプロキシ使用時にアバタークロップができない問題を修正 +- fix(server): メールアドレス更新時にバリデーションが正しく行われていないのを修正 +- fix(server): チャンネルでミュートが正しく機能していないのを修正 +- プッシュ通知でカスタム絵文字リアクションを表示できなかった問題を修正 + +## 13.8.1 (2023/02/26) + +### Bugfixes +- モバイルでドロワーメニューが表示されない問題を修正 + +## 13.8.0 (2023/02/26) + +### Improvements +- チャンネル内ハイライト +- ホームタイムラインのパフォーマンスを改善 +- renoteした際の表示を改善 +- バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない +- enhance(client): MkUrlPreviewの閉じるボタンを見やすく +- Add dialog to remove follower +- enhance(client): improve clip menu ux +- 検索画面の統合 +- enhance(client): ノートメニューからユーザーメニューを開けるように +- photoswipe 表示時に戻る操作をしても前の画面に戻らないように + +### Bugfixes +- Windows環境でswcを使うと正しくビルドできない問題の修正 +- fix(client): Android ChromeでPWAとしてインストールできない問題を修正 +- 未知のユーザーが deleteActor されたら処理をスキップする +- fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする +- fix(server): notes/createのバリデーションが機能していないのを修正 +- fix(server): エラーのスタックトレースは返さないように + +## 13.7.5 (2023/02/24) + +### Note +13.7.0以前から直接このバージョンにアップデートする場合は全ての通知が削除**されません。** + +### Improvements +- 紛らわしいため公開範囲の「ローカルのみ」オプションの名称を「連合なし」に変更 +- Frontend: スマホ・タブレットの場合、チャンネルの投稿フォームに自動でフォーカスしないように + +### Bugfixes +- 全ての通知が削除されてしまうのを修正 + +## 13.7.3 (2023/02/23) + +### Note +~~13.7.0以前から直接このバージョンにアップデートする場合は全ての通知が削除**されません。**~~ + +### Improvements + +### Bugfixes +- Client: 「キャッシュを削除」した後、ローカルのカスタム絵文字が表示されなくなるされなくなる問題を修正 +- Client: 通知設定画面で以前からグループの招待を有効化していた場合、通知の表示に失敗する問題の修正 +- Client: 通知設定画面に古いトグルが残っていた問題を修正 + +## 13.7.2 (2023/02/23) + +### Note +13.7.0以前からアップデートする場合は全ての通知が削除されます。 + +### Improvements +- enhance: make pwa icon maskable +- chore(client): tweak custom emoji size + +### Bugfixes +- マイグレーションが失敗することがあるのを修正 + +## 13.7.1 (2023/02/23) + +### Improvements +- pnpm buildではswcを使うように + +### Bugfixes +- NODE_ENV=productionでビルドできないのを修正 + +## 13.7.0 (2023/02/22) + +### Changes +- チャット機能が削除されました + +### Improvements +- Server: URLプレビュー(summaly)はプロキシを通すように +- Client: 2FA設定のUIをまともにした +- セキュリティキーの名前を変更できるように +- enhance(client): add quiz preset for play +- 広告開始時期を設定できるように +- みつけるで公開ロール一覧とそのメンバーを閲覧できるように +- enhance(client): MFMのx3, x4が含まれていたらノートをたたむように +- enhance(client): make possible to reload page of window + +### Bugfixes +- ユーザー検索ダイアログでローカルユーザーを絞って検索できない問題を修正 +- fix(client): MkHeader及びデッキのカラムでチャンネル一覧を選択したとき、最大5個までしか表示されない +- 管理画面の広告を10個以上見えるように +- Moderation note が保存できない +- ユーザーのハッシュタグ検索が機能していないのを修正 + +## 13.6.1 (2023/02/12) + +### Improvements +- アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化 +- Backend: activitypub情報がcorsでブロックされないようヘッダーを追加 +- enhance: レートリミットを0%にできるように +- チャンネル内Renoteを行えるように + +### Bugfixes +- Client: ユーザーページでアクティビティを見ることができない問題を修正 + +## 13.6.0 (2023/02/11) + +### Improvements +- MkPageHeaderをごっそり変えた + * モバイルではヘッダーは上下に分割され、下段にタブが表示されるように + * iconOnlyのタブ項目がアクティブな場合にはタブのタイトルを表示するように + * メインタイムラインではタイトルを表示しない + * メインタイムラインかつモバイルで表示される左上のアバターを選択するとアカウントメニューが開くように +- ユーザーページのノート一覧をタブとして分離 +- コンディショナルロールもバッジとして表示可能に +- enhance(client): ロールをより簡単に付与できるように +- enhance(client): 一度見たノートのRenoteは省略して表示するように +- enhance(client): 迷惑になる可能性のある投稿を行う前に警告を表示 +- リアクションの数が多い場合の表示を改善 +- 一部のMFM構文をopt-outに + +### Bugfixes +- Client: ユーザーページでタブがほとんど見れないことがないように + +## 13.5.6 (2023/02/10) + +### Improvements +- 非ログイン時にMiAuthを踏んだ際にMiAuthであることを表示する +- /auth/のUIをアップデート +- 利用規約同意UIの調整 +- クロップ時の質問を分かりやすく + +### Bugfixes +- fix: prevent clipping audio plyr's tooltip + ## 13.5.4 (2023/02/09) ### Improvements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e539926789..10d93cd9fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,11 +83,18 @@ An actual domain will be assigned so you can test the federation. - The title must be in the format `Release: x.y.z`. - `x.y.z` is the new version you are trying to release. 3. Deploy and perform a simple QA check. Also verify that the tests passed. -4. Merge it. +4. Merge it. (Do not squash commit) 5. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) - The target branch must be `master` - The tag name must be the version +> **Note** +> Why this instruction is necessary: +> - To perform final QA checks +> - To distribute responsibility +> - To check direct commits to develop +> - To celebrate the release together 🎉 + ## Localization (l10n) Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management. You can improve our translations with your Crowdin account. @@ -111,6 +118,26 @@ command. - Vite HMR (just the `vite` command) is available. The behavior may be different from production. - Service Worker is watched by esbuild. +### Dev Container +Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. +To use Dev Container, open the project directory on VSCode with Dev Containers installed. +**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled. + +It will run the following command automatically inside the container. +``` bash +git submodule update --init +pnpm install --frozen-lockfile +cp .devcontainer/devcontainer.yml .config/default.yml +pnpm build +pnpm migrate +``` + +After finishing the migration, run the `pnpm dev` command to start the development server. + +``` bash +pnpm dev +``` + ## Testing - Test codes are located in [`/packages/backend/test`](/packages/backend/test). @@ -258,9 +285,10 @@ SQLでは配列のインデックスは**1始まり**。 ### null IN nullが含まれる可能性のあるカラムにINするときは、そのままだとおかしくなるのでORなどでnullのハンドリングをしよう。 -### `undefined`にご用心 -MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。 -MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください +### enumの削除は気をつける +enumの列挙の内容の削除は、その値をもつレコードを全て削除しないといけない + +削除が重たかったり不可能だったりする場合は、削除しないでおく ### Migration作成方法 packages/backendで: @@ -271,6 +299,27 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o - 生成後、ファイルをmigration下に移してください - 作成されたスクリプトは不必要な変更を含むため除去してください +### JSON SchemaのobjectでanyOfを使うとき +JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。 +バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます) +https://github.com/misskey-dev/misskey/pull/10082 + +テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合: + +``` +export const paramDef = { + type: 'object', + properties: { + hoge: { type: 'string', minLength: 1 }, + fuga: { type: 'string', minLength: 1 }, + }, + anyOf: [ + { required: ['hoge'] }, + { required: ['fuga'] }, + ], +} as const; +``` + ### コネクションには`markRaw`せよ **Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。 diff --git a/Dockerfile b/Dockerfile index 0bfd24bd9a..b439716bea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +# syntax = docker/dockerfile:1.4 + ARG NODE_VERSION=18.13.0-bullseye FROM node:${NODE_VERSION} AS builder @@ -14,16 +16,16 @@ RUN corepack enable WORKDIR /misskey -COPY ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] -COPY ["scripts", "./scripts"] -COPY ["packages/backend/package.json", "./packages/backend/"] -COPY ["packages/frontend/package.json", "./packages/frontend/"] -COPY ["packages/sw/package.json", "./packages/sw/"] +COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] +COPY --link ["scripts", "./scripts"] +COPY --link ["packages/backend/package.json", "./packages/backend/"] +COPY --link ["packages/frontend/package.json", "./packages/frontend/"] +COPY --link ["packages/sw/package.json", "./packages/sw/"] RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output -COPY . ./ +COPY --link . ./ ARG NODE_ENV=production diff --git a/ROADMAP.md b/ROADMAP.md index c95bb8d92b..420f728758 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5,9 +5,9 @@ Also, the later tasks are more indefinite and are subject to change as developme ## (1) Improve maintainability \ This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development. -- Make the number of type errors zero (backend) +- ~~Make the number of type errors zero (backend)~~ → Done ✔️ - Improve CI - - Fix tests + - ~~Fix tests~~ → Done ✔️ - Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986 - Add more tests - ~~May need to implement a mechanism that allows for DI~~ → Done ✔️ diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 8f31cf7fb4..d151f1dd17 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -1,3 +1,4 @@ apiVersion: v2 name: misskey version: 0.0.0 +description: This chart is created for the purpose of previewing Pull Requests. Do not use this for production use. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..410f064be2 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,4 @@ +coverage: + status: + project: false + patch: false diff --git a/cypress/e2e/basic.cy.js b/cypress/e2e/basic.cy.js index eb5195c4b2..b1b856119c 100644 --- a/cypress/e2e/basic.cy.js +++ b/cypress/e2e/basic.cy.js @@ -10,14 +10,14 @@ describe('Before setup instance', () => { }); it('successfully loads', () => { - cy.visit('/'); + cy.visitHome(); }); it('setup instance', () => { - cy.visit('/'); + cy.visitHome(); cy.intercept('POST', '/api/admin/accounts/create').as('signup'); - + cy.get('[data-cy-admin-username] input').type('admin'); cy.get('[data-cy-admin-password] input').type('admin1234'); cy.get('[data-cy-admin-ok]').click(); @@ -43,11 +43,11 @@ describe('After setup instance', () => { }); it('successfully loads', () => { - cy.visit('/'); + cy.visitHome(); }); it('signup', () => { - cy.visit('/'); + cy.visitHome(); cy.intercept('POST', '/api/signup').as('signup'); @@ -79,11 +79,11 @@ describe('After user signup', () => { }); it('successfully loads', () => { - cy.visit('/'); + cy.visitHome(); }); it('signin', () => { - cy.visit('/'); + cy.visitHome(); cy.intercept('POST', '/api/signin').as('signin'); @@ -101,7 +101,7 @@ describe('After user signup', () => { userId: this.alice.id, }); - cy.visit('/'); + cy.visitHome(); cy.get('[data-cy-signin]').click(); cy.get('[data-cy-signin-username] input').type('alice'); @@ -112,7 +112,7 @@ describe('After user signup', () => { }); }); -describe('After user singed in', () => { +describe('After user signed in', () => { beforeEach(() => { cy.resetState(); @@ -141,6 +141,19 @@ describe('After user singed in', () => { cy.get('[data-cy-open-post-form-submit]').click(); cy.contains('Hello, Misskey!'); + }); + + it('open note form with hotkey', () => { + // Wait until the page loads + cy.get('[data-cy-open-post-form]').should('be.visible'); + // Use trigger() to give different `code` to test if hotkeys also work on non-QWERTY keyboards. + cy.document().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "n", code: "KeyL" }); + // See if the form is opened + cy.get('[data-cy-post-form-text]').should('be.visible'); + // Close it + cy.focused().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "Escape", code: "Escape" }); + // See if the form is closed + cy.get('[data-cy-post-form-text]').should('not.be.visible'); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 04a6d98b01..91a4d7abe6 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -24,6 +24,11 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +Cypress.Commands.add('visitHome', () => { + cy.visit('/'); + cy.get('button', { timeout: 30000 }).should('be.visible'); +}) + Cypress.Commands.add('resetState', () => { cy.window(win => { win.indexedDB.deleteDatabase('keyval-store'); @@ -43,7 +48,7 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => { }); Cypress.Commands.add('login', (username, password) => { - cy.visit('/'); + cy.visitHome(); cy.intercept('POST', '/api/signin').as('signin'); diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 5542e09b19..5254b20ef0 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -379,13 +379,10 @@ about: "عن" aboutMisskey: "عن Misskey" administrator: "المدير" token: "الرمز المميز" -twoStepAuthentication: "الإستيثاق بعاملَيْن" moderator: "مشرِف" moderation: "الإشراف" nUsersMentioned: "{n} مستخدمين أُشير إليهم" securityKey: "مفتاح الأمان" -securityKeyName: "اسم المفتاح" -registerSecurityKey: "سجل مفتاح أمان" lastUsed: "آخر استخدام" unregister: "إلغاء التسجيل" passwordLessLogin: "لِج مِن دون كلمة سرية" @@ -403,24 +400,15 @@ markAsReadAllTalkMessages: "علّم جميع الرسائل كمقروءة" help: "المساعدة" inputMessageHere: "اكتب رسالتك هنا" close: "اغلق" -group: "الفريق" -groups: "الفِرَق" -createGroup: "انشئ فريقًا" -ownedGroups: "فِرقي" -joinedGroups: "الفِرق المُنضم إليها" invites: "دعوة" -groupName: "اسم الفريق" members: "الأعضاء" transfer: "نقل" -messagingWithUser: "تحدث مع مستخدم" -messagingWithGroup: "محادثة جماعية" title: "العنوان" text: "النص" enable: "تشغيل" next: "التالية" retype: "أعد الكتابة" noteOf: "ملاحظات {user}" -inviteToGroup: "دعوة إلى فريق" quoteAttached: "اِقتُبسَ" quoteQuestion: "أتريد تضمينها كاقتباس" noMessagesYet: "ليس هناك رسائل بعد" @@ -442,14 +430,10 @@ passwordMatched: "التطابق صحيح!" passwordNotMatched: "غير متطابقتان" signinWith: "الولوج عبر {x}" signinFailed: "فشل الولوج، خطأ في اسم المستخدم أو كلمة المرور." -tapSecurityKey: "أنقر مفتاح الأمان" or: "أو" language: "اللغة" uiLanguage: "لغة واجهة المستخدم" -groupInvited: "دُعيت إلى فريقٍ" aboutX: "عن {x}" -youHaveNoGroups: "لا تمتلك أية فِرَق" -joinOrCreateGroup: "احصل على دعوة لفريق أو أنشئ واحدًا." noHistory: "السجل فارغ" signinHistory: "تاريخ تسجيل الدخول" doing: "انتظر لحظة" @@ -790,8 +774,6 @@ deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد الم incorrectPassword: "كلمة السر خاطئة." voteConfirm: "متيقِّن من تصويتك لـ {choice}؟" hide: "إخفاء" -leaveGroup: "مغادرة الفريق" -leaveGroupConfirm: "متيقن من مغادرة \"{name}\"؟" welcomeBackWithName: "مرحبًا بك مجددًا {name}" clickToFinishEmailVerification: "انقر [{ok}] لاستيثاق بريدك الإلكتروني." overridedDeviceKind: "نوع الجهاز" @@ -803,6 +785,7 @@ size: "الحجم" numberOfColumn: "عدد الأعمدة" searchByGoogle: "غوغل" mutePeriod: "مدة الكتم" +period: "ينتهي استطلاع الرأي في" indefinitely: "أبدًا" tenMinutes: "10 دقائق" oneHour: "ساعة" @@ -886,56 +869,6 @@ _nsfw: respect: "اخف الوسائط ذات المحتوى الحساس" ignore: "اعرض الوسائط ذات المحتوى الحساس" force: "اخف كل الوسائط" -_mfm: - cheatSheet: "مرجع ملخص عن MFM" - intro: "MFM هي لغة ترميزية مخصصة يمكن استخدامها في عدّة أماكن في ميسكي. يمكنك مراجعة كل تعابيرها مع كيفية استخدامها هنا." - mention: "أشر الى" - mentionDescription: "يمكنك الإشارة لمستخدم معيّن من خلال كتابة @ متبوعة باسم مستخدم." - hashtag: "الوسوم" - hashtagDescription: "يمكنك تعيين وسم من خلال كتابة # متبوعة بالنص المطلوب." - url: "الرابط" - urlDescription: "يمكن عرض الروابط" - link: "رابط" - bold: "عريض" - boldDescription: "جعل الحروف أثخن لإبرازها." - small: "صغير" - smallDescription: "يعرض المحتوى صغيرًا ورفيعًا." - center: "وسط" - centerDescription: "يمركز المحتوى في الوَسَط." - quote: "اقتبس" - quoteDescription: "يعرض المحتوى كاقتباس" - emoji: "إيموجي مخصص" - emojiDescription: "إحاطة اسم الإيموجي بنقطتي تفسير سيستبدله بصورة الإيموجي." - search: "البحث" - searchDescription: "يعرض نصًا في صندوق البحث" - flip: "اقلب" - flipDescription: "يقلب المحتوى عموديًا أو أفقيًا" - jelly: "تأثير (هلام)" - jellyDescription: "يمنح المحتوى حركة هلامية." - tada: "تأثير (تادا)" - tadaDescription: "يمنح للمحتوى تأثير تادا" - jump: "تأثير (قفز)" - jumpDescription: "يمنح للمحتوى حركة قفز." - bounce: "تأثير (ارتداد)" - bounceDescription: "يمنح للمحتوى حركة ارتدادية" - shake: "تأثير (اهتزاز)" - shakeDescription: "يمنح المحتوى حركة اهتزازية." - spin: "تأثير (دوران)" - spinDescription: "يمنح المحتوى حركة دورانية." - x2: "كبير" - x2Description: "يُكبر المحتوى" - x3: "كبير جداً" - x3Description: "يُضخم المحتوى" - x4: "هائل" - x4Description: "يُضخم المحتوى أكثر مما سبق." - blur: "طمس" - blurDescription: "يطمس المحتوى، لكن بالتمرير فوقه سيظهر بوضوح." - font: "الخط" - fontDescription: "الخط المستخدم لعرض المحتوى." - rainbow: "قوس قزح" - rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف" - rotate: "تدوير" - rotateDescription: "يُدير المحتوى بزاوية معيّنة." _instanceTicker: none: "لا تظهره بتاتًا" remote: "أظهر للمستخدمين البِعاد" @@ -1039,6 +972,7 @@ _ago: weeksAgo: "منذ {n} أسابيع" monthsAgo: "منذ {n} أشهر" yearsAgo: "منذ {n} سنوات" + invalid: "لا يوجد شيء هنا" _time: second: "ثا" minute: "د" @@ -1069,12 +1003,11 @@ _tutorial: step7_3: "حظًا سعيدًا واستمتع بوقتك مع ميسكي! 🚀" _2fa: alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين." - registerDevice: "سجّل جهازًا جديدًا" - registerKey: "تسجيل مفتاح أمان جديد" step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})." step2: "امسح رمز الاستجابة السريعة الموجد على الشاشة." step3: "أدخل الرمز الموجود في تطبيقك لإكمال التثبيت." step4: "من هذه اللحظة أثناء ولوجك سيُطلب منك الرمز." + renewTOTPCancel: "ليس اﻵن" _permissions: "read:account": "اعرض معلومات حسابك" "write:account": "تعديل معلومات حسابك" @@ -1183,8 +1116,6 @@ _visibility: followersDescription: "اجعلها مرئية لمتابِعيك فقط" specified: "مباشرة" specifiedDescription: "اجعلها مرئية لمستخدمين محددين" - localOnly: "المحلي فقط" - localOnlyDescription: "ليس مرئيًا للمستخدمين البِعاد" _postForm: replyPlaceholder: "رد على هذه الملاحظة…" quotePlaceholder: "اقتبس هذه الملاحظة…" @@ -1305,12 +1236,9 @@ _notification: youGotReply: "ردّ عليك {name}" youGotQuote: "اقتبس منك {name}" youRenoted: "إعادت نشر من {name}" - youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}" - youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}" youWereFollowed: "يتابعك" youReceivedFollowRequest: "تلقيتَ طلب متابعة" yourFollowRequestAccepted: "قُبل طلب المتابعة" - youWereInvitedToGroup: "دُعيت إلى فريقٍ" pollEnded: "ظهرت نتائج الاستطلاع" unreadAntennaNote: "هوائي {name}" _types: @@ -1323,7 +1251,6 @@ _notification: reaction: "التفاعلات" receiveFollowRequest: "طلبات المتابعة المتلقاة" followRequestAccepted: "طلبات المتابعة المقبولة" - groupInvited: "دعوات الفريق" app: "إشعارات التطبيقات المرتبطة" _actions: followBack: "تابعك بالمثل" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 6f5d676395..49b76b8ab3 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -382,12 +382,9 @@ about: "আপনার সম্পর্কে" aboutMisskey: "Misskey সম্পর্কে" administrator: "প্রশাসক" token: "টোকেন" -twoStepAuthentication: "২-ধাপ প্রমাণীকরণ" moderator: "মডারেটর" nUsersMentioned: "{n} জনকে উল্লেখ করা হয়েছে" securityKey: "সিকিউরিটি কী" -securityKeyName: "কী'র নাম" -registerSecurityKey: "সিকিউরিটি কী নিবন্ধন করুন" lastUsed: "শেষ ব্যাবহার করা হয়েছে" unregister: "নিবন্ধনমুক্ত হন" passwordLessLogin: "পাসওয়ার্ড-বিহীন লগইন সেট আপ করুন" @@ -405,24 +402,15 @@ markAsReadAllTalkMessages: "সমস্ত মেসেজ পঠিত হি help: "সহায়তা" inputMessageHere: "এখানে মেসেজ লিখুন" close: "বন্ধ" -group: "গ্রুপ" -groups: "গ্রুপসমূহ" -createGroup: "গ্রুপ তৈরী করুন" -ownedGroups: "আপনার গ্রুপগুলি" -joinedGroups: "যেসব গ্রুপে আপনি আছেন" invites: "আমন্ত্রণ" -groupName: "গ্রুপের নাম" members: "সদস্যবৃন্দ" transfer: "হস্তান্তর" -messagingWithUser: "প্রাইভেট চ্যাট" -messagingWithGroup: "গ্রুপ চ্যাট" title: "শিরোনাম" text: "পাঠ্য" enable: "সক্রিয়" next: "পরবর্তী" retype: "পুনঃ প্রবেশ" noteOf: "{user} এর নোট" -inviteToGroup: "গ্রুপে আমন্ত্রণ জানান" quoteAttached: "উদ্ধৃত" quoteQuestion: "উদ্ধৃতি হিসাবে সংযুক্ত করবেন?" noMessagesYet: "কোন মেসেজ নেই" @@ -444,18 +432,13 @@ passwordMatched: "মিলেছে" passwordNotMatched: "মিলেনি" signinWith: "{x} এর সাহায্যে সাইন ইন করুন" signinFailed: "লগ ইন করা যায়নি। আপনার ব্যবহারকারীর নাম এবং পাসওয়ার্ড চেক করুন." -tapSecurityKey: "সিকিউরিটি কী স্পর্শ করুন" or: "অথবা" language: "ভাষা" uiLanguage: "UI এর ভাষা" -groupInvited: "আপনি একটি গ্রুপে আমন্ত্রিত হয়েছেন" aboutX: "{x} সম্পর্কে" disableDrawer: "ড্রয়ার মেনু প্রদর্শন করবেন না" -youHaveNoGroups: "আপনার কোন গ্রুপ নেই " -joinOrCreateGroup: "একটি বিদ্যমান গ্রুপের আমন্ত্রণ পান বা একটি নতুন গ্রুপ তৈরি করুন৷" noHistory: "কোনো ইতিহাস নেই" signinHistory: "প্রবেশ করার ইতিহাস" -disableAnimatedMfm: "অ্যানিমেটেড MFM অক্ষম করুন" doing: "প্রক্রিয়া করছে..." category: "বিভাগ" tags: "ট‍্যাগসমূহ" @@ -821,8 +804,6 @@ deleteAccountConfirm: "আপনার অ্যাকাউন্ট মুছ incorrectPassword: "আপনার দেওয়া পাসওয়ার্ডটি ভুল।" voteConfirm: "\"{choice}\" এ ভোট দিতে চান?" hide: "লুকান" -leaveGroup: "গ্রুপ ছেড়ে চলে যান" -leaveGroupConfirm: "\"{name}\" গ্রুপ ছেড়ে চলে যেতে চান?" useDrawerReactionPickerForMobile: "মোবাইলে রিঅ্যাকশন পিকারকে ড্রয়ারে প্রদর্শন করুন" welcomeBackWithName: "আবার স্বাগতম, {name}" clickToFinishEmailVerification: " [{ok}] ক্লিক করার মাধ্যমে আপনার ইমেল ঠিকানা নিশ্চিত করুন।" @@ -838,6 +819,7 @@ instanceDefaultLightTheme: "ইন্সট্যান্সের ডিফল instanceDefaultDarkTheme: "ইন্সট্যান্সের ডিফল্ট ডার্ক থিম" instanceDefaultThemeDescription: "অবজেক্ট ফরম্যাটে থিম কোড লিখুন" mutePeriod: "মিউটের সময়কাল" +period: "পোলের সময়সীমা" indefinitely: "অনির্দিষ্ট" tenMinutes: "১০ মিনিট" oneHour: "১ ঘণ্টা" @@ -923,70 +905,6 @@ _nsfw: respect: "স্পর্শকাতর মিডিয়া লুকান" ignore: "স্পর্শকাতর মিডিয়া লুকাবেন না" force: "সকল মিডিয়া লুকান" -_mfm: - cheatSheet: "MFM চিটশিট" - intro: "MFM একটি মার্কআপ ভাষা যা Misskey-এর মধ্যে বিভিন্ন জায়গায় ব্যবহার করা যেতে পারে। এখানে আপনি MFM-এর সিনট্যাক্সগুলির একটি তালিকা দেখতে পারবেন।" - dummy: "মিসকি ফেডিভার্সের বিশ্বকে প্রসারিত করে" - mention: "উল্লেখ" - mentionDescription: "@ চিহ্ন + ব্যবহারকারীর নাম একটি নির্দিষ্ট ব্যবহারকারীকে নির্দেশ করতে ব্যবহার করা যায়।" - hashtag: "হ্যাশট্যাগ" - hashtagDescription: "আপনি একটি # চিহ্ন + ট্যাগ সহ একটি হ্যাশট্যাগ নির্দেশ করতে পারেন।" - url: "URL" - urlDescription: "URL দেখানো সম্ভব।" - link: "লিংক" - linkDescription: "আপনি পাঠ্যের একটি নির্দিষ্ট অংশকে URL হিসাবে দেখাতে পারেন৷" - bold: "গাঢ়" - boldDescription: "অক্ষরগুলিকে মোটাকরে প্রদর্শন করা হবে।" - small: "ছোট" - smallDescription: "লেখা ছোট এবং পাতলা করে দেখানো হবে।" - center: "সেন্টার" - centerDescription: "লেখা মাঝ বরাবর দেখানো হবে" - inlineCode: "কোড (ইনলাইন)" - inlineCodeDescription: " প্রোগ্রামের কোডের জন্য ইনলাইন সিনট্যাক্স হাইলাইটিং করা হবে" - blockCode: "কোড (ব্লক)" - blockCodeDescription: "মাল্টি-লাইন প্রোগ্রামের কোডের জন্য সিনট্যাক্স হাইলাইট করে।" - inlineMath: "গাণিতিক সূত্র (ইনলাইন)" - inlineMathDescription: "গাণিতিক সূত্র প্রদর্শন করুন (KaTeX) ইনলাইন।" - blockMath: "গাণিতিক সূত্র (ব্লক)" - blockMathDescription: "একটি ব্লকে একাধিক লাইনের গাণিতিক সূত্র প্রদর্শন করুন (KaTeX)।" - quote: "উদ্ধৃতি" - quoteDescription: "বিষয়বস্তুকে একটি উদ্ধৃতি হিসাবে দেখানো হবে।" - emoji: "স্বনির্ধারিত ইমোজিগুলি" - emojiDescription: "আপনি একটি কাস্টম ইমোজির নাম কোলনে আবদ্ধ করে কাস্টম ইমোজিটি দেখাতে পারেন৷" - search: "খুঁজুন" - searchDescription: "পূর্ব-টাইপ করা পাঠ্য সহ একটি অনুসন্ধান বাক্স প্রদর্শন করে।" - flip: "উল্টান" - flipDescription: "বিষয়বস্তু উপরে/নীচে বা বাম/ডানে উল্টান।" - jelly: "অ্যানিমেশন (জেলি)" - jellyDescription: "জেলির মত অ্যানিমেশন দেখায়।" - tada: "অ্যানিমেশন (টাডা)" - tadaDescription: "\"টাডা!\" এর মত অ্যানিমেশন দেখায়।" - jump: "অ্যানিমেশন (লাফ)" - jumpDescription: "বিষয়বস্তুতে লাফ মারার মত অ্যানিমেশন দেখায়।" - bounce: "অ্যানিমেশন (তিড়িং বিড়িং)" - bounceDescription: "তিড়িং বিড়িং করার মত অ্যানিমেশন দেখায়।" - shake: "অ্যানিমেশন (ঝাঁকি)" - shakeDescription: "ঝাঁকির মত অ্যানিমেশন দেখায়।" - twitch: "অ্যানিমেশন (মোচড়ানো)" - twitchDescription: "মোচড়ানোর মত অ্যানিমেশন দেখায়।" - spin: "অ্যানিমেশন (ঘুরা)" - spinDescription: "ঘুরার মত অ্যানিমেশন দেখায়।" - x2: "বড়" - x2Description: "বিষয়বস্তু বড় করে দেখায়।" - x3: "অনেক বড়" - x3Description: "বিষয়বস্তু আরও বড় করে দেখায়।" - x4: "অস্বাভাবিক বড়" - x4Description: "বিষয়বস্তুকে আগের থেকেও আরও বড় করে দেখায়।" - blur: "ব্লার" - blurDescription: "বিষয়বস্তুকে ব্লার করতে পারেন। আপনি এর উপর মাউস কার্সার রাখলে, এটি পরিষ্কারভাবে দেখতে পাবেন।" - font: "ফন্ট" - fontDescription: "বিষয়বস্তুকে কোন ফন্টে দেখানো হবে তা নির্ধারণ করে।" - rainbow: "রেইনবো" - rainbowDescription: "বিষয়বস্তুকে রংধনুর রং গুলিতে প্রদর্শন করে।" - sparkle: "চিক চিক" - sparkleDescription: "বিষয়বস্তুকে একটি চিকচিকে কণা প্রভাব দেয়।" - rotate: "ঘুরান" - rotateDescription: "বিষয়বস্তুকে একটি নির্দিষ্ট কোনে ঘুরায়।" _instanceTicker: none: "দেখাবেন না" remote: "রিমোট ব্যাবহারকারীদের জন্য দেখান" @@ -1116,6 +1034,7 @@ _ago: weeksAgo: "{n} সপ্তাহ আগে" monthsAgo: "{n} মাস আগে" yearsAgo: "{n} বছর আগে" + invalid: "এখানে কিছুই নাই" _time: second: "সেকেন্ড" minute: "মিনিট" @@ -1146,8 +1065,6 @@ _tutorial: step7_3: "এখন Misskey উপভোগ করুন 🚀" _2fa: alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷" - registerDevice: "নতুন ডিভাইস নিবন্ধন করুন" - registerKey: "সিকিউরিটি কী নিবন্ধন করুন" step1: "প্রথমে, আপনার ডিভাইসে {a} বা {b} এর মতো একটি অথেনটিকেশন অ্যাপ ইনস্টল করুন৷" step2: "এরপরে, অ্যাপের সাহায্যে প্রদর্শিত QR কোডটি স্ক্যান করুন।" step2Url: "ডেস্কটপ অ্যাপে, নিম্নলিখিত URL লিখুন:" @@ -1199,7 +1116,6 @@ _antennaSources: homeTimeline: "আপনি অনুসরণ করছেন, এমন ব্যবহারকারীদের নোট" users: "এক বা একাধিক নির্দিষ্ট ব্যবহারকারীর নোট" userList: "নির্দিষ্ট তালিকায় নাম থাকা ব্যবহারকারীদের নোট" - userGroup: "নির্দিষ্ট গ্রুপে থাকা ব্যবহারকারীদের নোট" _weekday: sunday: "রবিবার" monday: "সোমবার" @@ -1268,8 +1184,6 @@ _visibility: followersDescription: "শুধুমাত্র আপনার অনুসরণকারীদের নিকট পোস্ট করুন" specified: "ডাইরেক্ট নোট" specifiedDescription: "শুধুমাত্র নির্দিষ্ট ব্যাবহারকারীর নিকট পাঠান" - localOnly: "শুধুমাত্র লোকাল" - localOnlyDescription: "রিমোট ব্যাবহারকারীদের নিকট দৃশ্যমান নয়" _postForm: replyPlaceholder: "নোটটির জবাব দিন..." quotePlaceholder: "নোটটিকে উদ্ধৃত করুন..." @@ -1397,12 +1311,9 @@ _notification: youGotReply: "{name} আপনাকে জবাব দিয়েছে" youGotQuote: "{name} আপনাকে উদ্ধৃত করেছে" youRenoted: "{name} এর Renote" - youGotMessagingMessageFromUser: "{name} আপনাকে মেসেজ করেছে" - youGotMessagingMessageFromGroup: "{name} গ্রুপে একটি নতুন মেসেজ আছে" youWereFollowed: "আপনাকে অনুসরণ করছে" youReceivedFollowRequest: "অনুসরণ করার জন্য অনুরোধ পাওয়া গেছে" yourFollowRequestAccepted: "আপনার অনুসরণ করার অনুরোধ গৃহীত হয়েছে" - youWereInvitedToGroup: "আপনি একটি গ্রুপে আমন্ত্রিত হয়েছেন" pollEnded: "পোলের ফলাফল দেখা যাবে" emptyPushNotificationMessage: "আপডেট করা পুশ বিজ্ঞপ্তি" _types: @@ -1416,7 +1327,6 @@ _notification: pollEnded: "পোল শেষ" receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ" followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" - groupInvited: "গ্রুপের আমন্ত্রনসমূহ" app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি" _actions: followBack: "ফলো ব্যাক করেছে" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 8bc5bf0366..2b1168f780 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -315,13 +315,10 @@ userList: "Llistes" about: "Informació" aboutMisskey: "Quant a Misskey" administrator: "Administrador/a" -twoStepAuthentication: "Verificació en dos passos" moderator: "Moderador/a" moderation: "Moderació" nUsersMentioned: "{n} usuaris mencionats" securityKey: "Clau de seguretat" -securityKeyName: "Nom de la clau" -registerSecurityKey: "Registra la clau de seguretat" unregister: "Cancel·la el registre" passwordLessLogin: "Inici de sessió sense contrasenya" resetPassword: "Restableix la contrasenya" @@ -334,7 +331,6 @@ help: "Ajuda" invites: "Convida" next: "Següent" noteOf: "Publicació de: {user}" -inviteToGroup: "Convida'l al grup" invitations: "Convida" tags: "Etiquetes" docSource: "Font del document" @@ -375,11 +371,6 @@ file: "Fitxers" _email: _follow: title: "t'ha seguit" -_mfm: - mention: "Menció" - quote: "Citar" - emoji: "Emojis personalitzats" - search: "Cercar" _instanceMute: instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat." _theme: @@ -398,7 +389,6 @@ _antennaSources: homeTimeline: "Publicacions dels usuaris seguits" users: "Publicacions d'usuaris específics" userList: "Publicacions d'una llista d'usuaris" - userGroup: "Publicacions d'usuaris d'un grup" _widgets: profile: "Perfil" instanceInfo: "Informació del fitxer d'instal·lació" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 926c173f8c..7f665895b9 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -337,12 +337,9 @@ about: "Informace" aboutMisskey: "O Misskey" administrator: "Administrátor" token: "Token" -twoStepAuthentication: "Dvoufaktorová autentikace" moderator: "Moderátor" nUsersMentioned: "{n} uživatelů zmínilo" securityKey: "Bezpečnostní klíč" -securityKeyName: "Název klíče" -registerSecurityKey: "Registrovat bezpečnostní klíč" lastUsed: "Naposledy použito" unregister: "Odstranit" resetPassword: "Resetovat heslo" @@ -359,13 +356,7 @@ markAsReadAllTalkMessages: "Označit všechny zprávy za přečtené" help: "Nápověda" inputMessageHere: "Sem zadejte zprávu" close: "Zavřít" -group: "Skupina" -groups: "Skupiny" -createGroup: "Vytvořit skupinu" -ownedGroups: "Vlastněné skupiny" -joinedGroups: "Členství ve skupinách" invites: "Pozvat" -groupName: "Název skupiny" members: "Členové" transfer: "Převod" title: "Titulek" @@ -374,7 +365,6 @@ enable: "Povolit" next: "Další" retype: "Zadejte znovu" noteOf: "{user} poznámky" -inviteToGroup: "Pozvat do skupiny" quoteAttached: "Citace" quoteQuestion: "Přiložit jako citaci?" noMessagesYet: "Zatím tu nejsou žádné zprávy" @@ -396,14 +386,10 @@ passwordMatched: "Hesla se schodují" passwordNotMatched: "Hesla se neschodují" signinWith: "Přihlásit se s {x}" signinFailed: "Nelze se přihlásit. Zkontrolujte prosím své uživatelské jméno a heslo." -tapSecurityKey: "Ťukněte na bezpečnostní klíč" or: "Nebo" language: "Jazyk" uiLanguage: "Jazyk uživatelského rozhraní" -groupInvited: "Pozvat do skupiny" aboutX: "O {x}" -youHaveNoGroups: "Nemáte žádné skupiny" -joinOrCreateGroup: "Můžete požádat o pozvání do stávající skupiny nebo vytvořit novou." noHistory: "Žádná historie" signinHistory: "Historie přihlášení" category: "Kategorie" @@ -642,19 +628,6 @@ _registry: _aboutMisskey: allContributors: "Všichni přispěvatelé" source: "Zdrojový kód" -_mfm: - mention: "Zmínění" - hashtag: "Hashtag" - link: "Odkaz" - bold: "Tučně" - quote: "Citovat" - emoji: "Vlastní emoji" - search: "Vyhledávání" - flip: "Otočit" - tada: "Animace (tadá)" - blur: "Rozmazání" - font: "Font" - rainbow: "Duha" _channel: featured: "Trendy" _menuDisplay: @@ -686,13 +659,11 @@ _sfx: _ago: future: "Budoucí" justNow: "Teď" + invalid: "Nic nebylo nalezeno" _time: second: "Sekund" minute: "Minut" hour: "Hodin" -_2fa: - registerDevice: "Přidat zařízení" - registerKey: "Přidat bezpečnostní klíč" _weekday: sunday: "Neděle" monday: "Pondělí" @@ -786,7 +757,6 @@ _pages: button: "Tlačítko" _notification: youWereFollowed: "Máte nového následovníka" - youWereInvitedToGroup: "Pozvat do skupiny" _types: all: "Vše" follow: "Sledovaní" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 85c7a3e63f..c5ddf334c4 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -103,6 +103,8 @@ renoted: "Renote getätigt." cantRenote: "Renote dieses Beitrags nicht möglich." cantReRenote: "Renote einer Renote nicht möglich." quote: "Zitieren" +inChannelRenote: "Kanal-interner Renote" +inChannelQuote: "Kanal-internes Zitat" pinnedNote: "Angeheftete Notiz" pinned: "Angeheftet" you: "Du" @@ -257,6 +259,8 @@ noMoreHistory: "Kein weiterer Verlauf vorhanden" startMessaging: "Neuen Chat erstellen" nUsersRead: "Von {n} Benutzern gelesen" agreeTo: "Ich stimme {0} zu" +agreeBelow: "Ich stimme Untenstehendem zu" +basicNotesBeforeCreateAccount: "Wichtige Infos" tos: "Nutzungsbedingungen" start: "Anfangen" home: "Startseite" @@ -389,16 +393,19 @@ about: "Über" aboutMisskey: "Über Misskey" administrator: "Administrator" token: "Token" -twoStepAuthentication: "Zwei-Faktor-Authentifizierung" +2fa: "Zwei-Faktor-Authentifizierung" +totp: "Authentifizierungs-App" +totpDescription: "Logge dich via Authentifizierungs-App mit Einmalpasswort ein" moderator: "Moderator" moderation: "Moderation" nUsersMentioned: "Von {n} Benutzern erwähnt" +securityKeyAndPasskey: "Security-Tokens und Passkeys" securityKey: "Sicherheitsschlüssel" -securityKeyName: "Schlüsselname" -registerSecurityKey: "Sicherheitsschlüssel registrieren" lastUsed: "Zuletzt benutzt" +lastUsedAt: "Zuletzt verwendet: {t}" unregister: "Deaktivieren" passwordLessLogin: "Passwortloses Anmelden einrichten" +passwordLessLoginDescription: "Ermöglicht passwortfreies Einloggen, nur via Security-Token oder Passkey" resetPassword: "Passwort zurücksetzen" newPasswordIs: "Das neue Passwort ist „{password}“" reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren" @@ -413,24 +420,15 @@ markAsReadAllTalkMessages: "Alle Chats als gelesen markieren" help: "Hilfe" inputMessageHere: "Hier Nachricht eingeben" close: "Schließen" -group: "Gruppe" -groups: "Gruppen" -createGroup: "Gruppe erstellen" -ownedGroups: "Meine Gruppen" -joinedGroups: "Beigetretene Gruppen" invites: "Einladungen" -groupName: "Gruppenname" members: "Mitglieder" transfer: "Übertragen" -messagingWithUser: "Privatchat" -messagingWithGroup: "Gruppenchat" title: "Titel" text: "Text" enable: "Aktivieren" next: "Weiter" retype: "Erneut eingeben" noteOf: "Notiz von {user}" -inviteToGroup: "Zu Gruppe einladen" quoteAttached: "Zitat" quoteQuestion: "Als Zitat anhängen?" noMessagesYet: "Noch keine Nachrichten vorhanden" @@ -452,20 +450,18 @@ passwordMatched: "Stimmt überein" passwordNotMatched: "Stimmt nicht überein" signinWith: "Mit {x} anmelden" signinFailed: "Anmeldung fehlgeschlagen. Überprüfe Benutzername und Passswort." -tapSecurityKey: "Tippe deinen Sicherheitsschlüssel an" or: "Oder" language: "Sprache" uiLanguage: "Sprache der Benutzeroberfläche" -groupInvited: "Du wurdest in eine Gruppe eingeladen" aboutX: "Über {x}" emojiStyle: "Emoji-Stil" native: "Nativ" disableDrawer: "Keine ausfahrbaren Menüs verwenden" -youHaveNoGroups: "Keine Gruppen vorhanden" -joinOrCreateGroup: "Lass dich zu einer Gruppe einladen oder erstelle deine eigene." +showNoteActionsOnlyHover: "Aktionen für Notizen nur bei Mouseover anzeigen" noHistory: "Kein Verlauf gefunden" signinHistory: "Anmeldungsverlauf" -disableAnimatedMfm: "MFM, die Animationen enthalten, deaktivieren" +enableAdvancedMfm: "Erweitertes MFM aktivieren" +enableAnimatedMfm: "Animiertes MFM aktivieren" doing: "In Bearbeitung …" category: "Kategorie" tags: "Schlagwörter" @@ -784,6 +780,7 @@ popularPosts: "Beliebte Beiträge" shareWithNote: "Mit Notiz teilen" ads: "Werbung" expiration: "Frist" +startingperiod: "Start" memo: "Merkzettel" priority: "Priorität" high: "Hoch" @@ -816,6 +813,7 @@ lastCommunication: "Letzte Kommunikation" resolved: "Gelöst" unresolved: "Ungelöst" breakFollow: "Follower entfernen" +breakFollowConfirm: "Diesen Follower wirklich entfernen?" itsOn: "Eingeschaltet" itsOff: "Ausgeschaltet" emailRequiredForSignup: "Angabe einer Email-Adresse als benötigt markieren" @@ -835,8 +833,6 @@ deleteAccountConfirm: "Dein Benutzerkonto wird unwiderruflich gelöscht. Trotzde incorrectPassword: "Falsches Passwort." voteConfirm: "Wirklich für „{choice}“ abstimmen?" hide: "Inhalt verbergen" -leaveGroup: "Gruppe verlassen" -leaveGroupConfirm: "Möchtest du „{name}“ wirklich verlassen?" useDrawerReactionPickerForMobile: "Auf mobilen Geräten ausfahrbare Reaktionsauswahl anzeigen" welcomeBackWithName: "Willkommen zurück, {name}" clickToFinishEmailVerification: "Drücke bitte auf [{ok}], um die Email-Bestätigung abzuschließen." @@ -852,16 +848,20 @@ instanceDefaultLightTheme: "Instanzweites Standardfarbschema (Hell)" instanceDefaultDarkTheme: "Instanzweites Standardfarbschema (Dunkel)" instanceDefaultThemeDescription: "Gib den Farbschemencode im Objektformat ein." mutePeriod: "Stummschaltungsdauer" +period: "Zeitlimit" indefinitely: "Dauerhaft" tenMinutes: "10 Minuten" oneHour: "Eine Stunde" oneDay: "Einen Tag" oneWeek: "Eine Woche" +oneMonth: "1 Monat" reflectMayTakeTime: "Es kann etwas dauern, bis sich dies widerspiegelt." failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgefragt werden" rateLimitExceeded: "Versuchsanzahl überschritten" cropImage: "Bild zuschneiden" cropImageAsk: "Möchtest du das Bild zuschneiden?" +cropYes: "Zuschneiden" +cropNo: "Unbearbeitet verwenden" file: "Datei" recentNHours: "Letzten {n} Stunden" recentNDays: "Letzten {n} Tage" @@ -940,6 +940,21 @@ cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes preset: "Vorlage" selectFromPresets: "Aus Vorlagen wählen" achievements: "Errungenschaften" +gotInvalidResponseError: "Ungültige Antwort des Servers" +gotInvalidResponseErrorDescription: "Eventuell ist der Server momentan nicht erreichbar oder untergeht Wartungsarbeiten. Bitte versuche es später noch einmal." +thisPostMayBeAnnoying: "Dieser Beitrag stört eventuell andere Benutzer." +thisPostMayBeAnnoyingHome: "Zur Startseite schicken" +thisPostMayBeAnnoyingCancel: "Abbrechen" +thisPostMayBeAnnoyingIgnore: "Trotzdem schicken" +collapseRenotes: "Bereits gesehene Renotes verkürzt anzeigen" +internalServerError: "Serverinterner Fehler" +internalServerErrorDescription: "Im Server ist ein unerwarteter Fehler aufgetreten." +copyErrorInfo: "Fehlerdetails kopieren" +joinThisServer: "Bei dieser Instanz registrieren" +exploreOtherServers: "Eine andere Instanz finden" +letsLookAtTimeline: "Die Chronik durchstöbern" +disableFederationWarn: "Dies deaktiviert Föderation, aber alle Notizen bleiben, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird diese Option nicht benötigt." +invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst einen validen Einladungscode eingeben, um dich zu registrieren." _achievements: earnedAt: "Freigeschaltet am" _types: @@ -1324,72 +1339,6 @@ _nsfw: respect: "Als NSFW markierte Bilder verbergen" ignore: "Als NSFW markierte Bilder nicht verbergen" force: "Alle Medien verbergen" -_mfm: - cheatSheet: "MFM Spickzettel" - intro: "MFM ist eine Misskey-exklusive Markup-Sprache, die in Misskey an vielen Stellen verwendet werden kann. Hier kannst du eine Liste von verfügbarer MFM-Syntax einsehen." - dummy: "Misskey erweitert die Welt des Fediverse" - mention: "Erwähnung" - mentionDescription: "Mit At-Zeichen und Benutzername kann ein individueller Nutzer angegeben werden." - hashtag: "Hashtag" - hashtagDescription: "Mit einer Raute und Text kann ein Hashtag angegeben werden." - url: "URL" - urlDescription: "Zeigt URLs an." - link: "Link" - linkDescription: "Zeigt spezifische Textabschnitte als URL an." - bold: "Fett" - boldDescription: "Zeichen zur Betonung dicker erscheinen lassen." - small: "Klein" - smallDescription: "Inhalt klein und dünn erscheinen lassen." - center: "Zentrieren" - centerDescription: "Inhalt zentriert anzeigen." - inlineCode: "Code (Eingebettet)" - inlineCodeDescription: "Syntax-Hervorhebung für (Programm-)Code eingebettet anzeigen." - blockCode: "Code (Block)" - blockCodeDescription: "Syntax-Hervorhebung für mehrzeiligen (Programm-)Code als Block anzeigen." - inlineMath: "Mathe (Eingebettet)" - inlineMathDescription: "Mathematische Formeln (KaTeX) eingebettet anzeigen." - blockMath: "Mathe (Block)" - blockMathDescription: "Mehrzeilige mathematische Formeln (KaTeX) als Block einbetten." - quote: "Zitationen" - quoteDescription: "Inhalt als Zitat anzeigen." - emoji: "Benutzerdefinierte Emojis" - emojiDescription: "Durch das Umschließen von Emoji-Namen durch Doppelpunkte können benutzerdefinierte Emojis angezeigt werden." - search: "Suche" - searchDescription: "Eine vorgefertige Suchanfragebox anzeigen." - flip: "Spiegelung" - flipDescription: "Inhalt horizontal oder vertikal gespiegelt anzeigen." - jelly: "Animation (Dehnen)" - jellyDescription: "Verleiht Inhalt eine sich dehnende Animation." - tada: "Animation (Tada)" - tadaDescription: "Verleiht Inhalt eine Animation mit \"Tada!\"-Gefühl" - jump: "Animation (Sprung)" - jumpDescription: "Verleiht Inhalt eine springende Animation." - bounce: "Animation (Federn)" - bounceDescription: "Verleiht Inhalt eine federnde Animation." - shake: "Animation (Zittern)" - shakeDescription: "Verleiht Inhalt eine zitternde Animation." - twitch: "Animation (Zucken)" - twitchDescription: "Verleiht Inhalt eine sehr stark zuckende Animation." - spin: "Animation (Rotieren)" - spinDescription: "Verleiht Inhalt eine rotierende Animation." - x2: "Groß" - x2Description: "Inhalte größer anzeigen." - x3: "Sehr groß" - x3Description: "Inhalte noch größer anzeigen." - x4: "Unglaublich groß" - x4Description: "Lässt Inhalte noch größer als größer als groß angezeigt werden." - blur: "Weichzeichnen" - blurDescription: "Inhalte durch Weihzeichnung verschwimmen lassen. Durch das Bewegen des Mauszeigers über den Inhalt wird er klar angezeigt." - font: "Schriftart" - fontDescription: "Setzt die Schriftart des Inhaltes fest." - rainbow: "Regenbogen" - rainbowDescription: "Lässt den Inhalt in Regenbogenfarben erscheinen." - sparkle: "Glitzer" - sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt." - rotate: "Drehen" - rotateDescription: "Dreht den Inhalt um einen angegebenen Winkel." - plain: "Schlicht" - plainDescription: "Deaktiviert jegliche MFM-Syntax, die sich innerhalb dieses MFM-Effekts befindet." _instanceTicker: none: "Nie anzeigen" remote: "Für Benutzer fremder Instanzen anzeigen" @@ -1519,6 +1468,7 @@ _ago: weeksAgo: "vor {n} Woche(n)" monthsAgo: "vor {n} Monat(en)" yearsAgo: "vor {n} Jahr(en)" + invalid: "Ungültig" _time: second: "Sekunde(n)" minute: "Minute(n)" @@ -1552,14 +1502,29 @@ _tutorial: step8_3: "Diese Einstellung kannst du jederzeit ändern." _2fa: alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert." - registerDevice: "Neues Gerät registrieren" - registerKey: "Neuen Sicherheitsschlüssel registrieren" + registerTOTP: "Authentifizierungs-App registrieren" + passwordToTOTP: "Bitte Passwort eingeben" step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät." step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät." + step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren." step2Url: "Nutzt du ein Desktopprogramm kannst du alternativ diese URL eingeben:" + step3Title: "Authentifizierungsscode eingeben" step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird." step4: "Alle folgenden Anmeldungsversuche werden ab sofort die Eingabe eines solchen Tokens benötigen." + securityKeyNotSupported: "Dein Browser unterstützt keine Security-Tokens." + registerTOTPBeforeKey: "Um einen Security-Token oder einen Passkey zu registrieren, musst du zuerst eine Authentifizierungs-App registrieren." securityKeyInfo: "Du kannst neben Fingerabdruck- oder PIN-Authentifizierung auf deinem Gerät auch Anmeldung mit Hilfe eines FIDO2-kompatiblen Hardware-Sicherheitsschlüssels einrichten." + chromePasskeyNotSupported: "Chrome-Passkeys werden zur Zeit nicht unterstützt." + registerSecurityKey: "Security-Token oder Passkey registrieren" + securityKeyName: "Schlüsselname eingeben" + tapSecurityKey: "Bitten folge den Anweisungen deines Browsers zur Registrierung" + removeKey: "Sicherheitsschlüssel entfernen" + removeKeyConfirm: "Den Schlüssel {name} wirklich löschen?" + whyTOTPOnlyRenew: "Solange ein Sicherheitsschlüssel registriert ist, kann die Authentifizierungs-App nicht entfernt werden." + renewTOTP: "Authentifizierungs-App neu einrichten" + renewTOTPConfirm: "Codes der bisherigen App werden hierdurch nutzlos" + renewTOTPOk: "Neu einrichten" + renewTOTPCancel: "Abbrechen" _permissions: "read:account": "Deine Benutzerkontoinformationen lesen" "write:account": "Deine Benutzerkontoinformationen bearbeiten" @@ -1594,18 +1559,20 @@ _permissions: "read:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge lesen" "write:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge bearbeiten" _auth: + shareAccessTitle: "Verteilung von App-Berechtigungen" shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Benutzerkonto zugreifen zu können?" shareAccessAsk: "Bist du dir sicher, dass du diese Anwendung authorisieren möchtest, auf dein Benutzerkonto zugreifen zu können?" + permission: "{name} fordert folgende Berechtigungen" permissionAsk: "Diese Anwendung fordert folgende Berechtigungen" pleaseGoBack: "Bitte kehre zur Anwendung zurück" callback: "Es wird zur Anwendung zurückgekehrt" denied: "Zugriff verweigert" + pleaseLogin: "Bitte logge dich ein, um Apps zu authorisieren." _antennaSources: all: "Alle Notizen" homeTimeline: "Notizen von Benutzern, denen gefolgt wird" users: "Notizen von einem oder mehreren angegebenen Benutzern" userList: "Notizen von allen Benutzern einer Liste" - userGroup: "Notizen von allen Benutzern einer Gruppe" _weekday: sunday: "Sonntag" monday: "Montag" @@ -1680,8 +1647,8 @@ _visibility: followersDescription: "Nur für Follower sichtbar" specified: "Direkt" specifiedDescription: "Nur für bestimmte Benutzer sichtbar" - localOnly: "Nur Lokal" - localOnlyDescription: "Unsichtbar für Benutzer anderer Instanzen" + disableFederation: "Deförderiert" + disableFederationDescription: "Nicht an andere Instanzen übertragen" _postForm: replyPlaceholder: "Dieser Notiz antworten …" quotePlaceholder: "Diese Notiz zitieren …" @@ -1819,12 +1786,9 @@ _notification: youGotReply: "{name} hat dir geantwortet" youGotQuote: "{name} hat dich zitiert" youRenoted: "Renote deiner Notiz von {name}" - youGotMessagingMessageFromUser: "{name} hat dir eine Chatnachricht gesendet" - youGotMessagingMessageFromGroup: "In die Gruppe {name} wurde eine Chatnachricht gesendet" youWereFollowed: "ist dir gefolgt" youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten" yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert" - youWereInvitedToGroup: "{userName} hat dich in eine Gruppe eingeladen" pollEnded: "Umfrageergebnisse sind verfügbar" unreadAntennaNote: "Antenne {name}" emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert" @@ -1840,7 +1804,7 @@ _notification: pollEnded: "Ende von Umfragen" receiveFollowRequest: "Erhaltene Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen" - groupInvited: "Erhaltene Gruppeneinladungen" + achievementEarned: "Errungenschaft freigeschaltet" app: "Benachrichtigungen von Apps" _actions: followBack: "folgt dir nun auch" @@ -1873,3 +1837,6 @@ _deck: channel: "Kanal" mentions: "Erwähnungen" direct: "Direktnachrichten" +_dialog: + charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}" + charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index c711683ffc..0721ba6e99 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -230,21 +230,13 @@ moderator: "Συντονιστής" moderation: "Συντονισμός" cacheClear: "Εκκαθάριση προσωρινής μνήμης" markAsReadAllNotifications: "Όλες οι ειδοποιήσεις διαβάστηκαν" -group: "Ομάδα" -groups: "Ομάδες" -createGroup: "Δημιουργία ομάδας" -ownedGroups: "Οι ομάδες σας" -groupName: "Όνομα ομάδας" members: "Μέλη" transfer: "Μεταφορά" -messagingWithUser: "Ιδιωτική συνομιλία" -messagingWithGroup: "Ομαδική συνομιλία" title: "Τίτλος" text: "Κείμενο" enable: "Ενεργοποίηση" next: "Επόμενο" noteOf: "Σημείωμα από {user}" -inviteToGroup: "Πρόσκληση στην ομάδα" quoteAttached: "Παράθεση" signinRequired: "Παρακαλούμε δημιουργήστε λογαριασμό ή συνδεθείτε πριν συνεχίσετε" category: "Κατηγορία" @@ -298,11 +290,6 @@ cannotUploadBecauseNoFreeSpace: "Το ανέβασμα απέτυχε λόγω _email: _follow: title: "Έχετε ένα νέο ακόλουθο" -_mfm: - mention: "Επισήμανση" - quote: "Παράθεση" - emoji: "Επιπλέον emoji" - search: "Αναζήτηση" _channel: featured: "Δημοφιλή" _theme: @@ -342,7 +329,6 @@ _antennaSources: homeTimeline: "Σημειώματα από μέλη που ακολουθείτε" users: "Σημειώματα από συγκεκριμένα μέλη" userList: "Σημειώματα από καθορισμένη λίστα μελών" - userGroup: "Σημειώματα από μέλη καθορισμένης ομάδας" _widgets: profile: "Προφίλ" instanceInfo: "Πληροφορίες του instance" @@ -387,7 +373,6 @@ _pages: blocks: image: "Εικόνες" _notification: - youGotMessagingMessageFromUser: "{name} σάς έστειλε ένα μήνυμα συνομιλίας" youWereFollowed: "σε ακολούθησε" _types: follow: "Νέοι ακόλουθοι" diff --git a/locales/en-US.yml b/locales/en-US.yml index d9a34b8999..638c470916 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -103,6 +103,8 @@ renoted: "Renoted." cantRenote: "This post can't be renoted." cantReRenote: "A renote can't be renoted." quote: "Quote" +inChannelRenote: "Channel-only Renote" +inChannelQuote: "Channel-only Quote" pinnedNote: "Pinned note" pinned: "Pin to profile" you: "You" @@ -257,6 +259,8 @@ noMoreHistory: "There is no further history" startMessaging: "Start a new chat" nUsersRead: "read by {n}" agreeTo: "I agree to {0}" +agreeBelow: "I agree to the below" +basicNotesBeforeCreateAccount: "Important notes" tos: "Terms of Service" start: "Begin" home: "Home" @@ -389,16 +393,19 @@ about: "About" aboutMisskey: "About Misskey" administrator: "Administrator" token: "Token" -twoStepAuthentication: "Two-factor authentication" +2fa: "Two-factor authentication" +totp: "Authenticator App" +totpDescription: "Use an authenticator app to enter one-time passwords" moderator: "Moderator" moderation: "Moderation" nUsersMentioned: "Mentioned by {n} users" +securityKeyAndPasskey: "Security- and passkeys" securityKey: "Security key" -securityKeyName: "Key name" -registerSecurityKey: "Register a security key" lastUsed: "Last used" +lastUsedAt: "Last used: {t}" unregister: "Unregister" passwordLessLogin: "Password-less login" +passwordLessLoginDescription: "Allows password-less login using a security- or passkey only" resetPassword: "Reset password" newPasswordIs: "The new password is \"{password}\"" reduceUiAnimation: "Reduce UI animations" @@ -413,24 +420,15 @@ markAsReadAllTalkMessages: "Mark all messages as read" help: "Help" inputMessageHere: "Enter message here" close: "Close" -group: "Group" -groups: "Groups" -createGroup: "Create a group" -ownedGroups: "Owned Groups" -joinedGroups: "Joined groups" invites: "Invites" -groupName: "Group name" members: "Members" transfer: "Transfer" -messagingWithUser: "Private chat" -messagingWithGroup: "Group chat" title: "Title" text: "Text" enable: "Enable" next: "Next" retype: "Enter again" noteOf: "Note by {user}" -inviteToGroup: "Invite to group" quoteAttached: "Quote" quoteQuestion: "Append as quote?" noMessagesYet: "No messages yet" @@ -452,20 +450,18 @@ passwordMatched: "Matches" passwordNotMatched: "Does not match" signinWith: "Sign in with {x}" signinFailed: "Unable to sign in. The entered username or password is incorrect." -tapSecurityKey: "Tap your security key" or: "Or" language: "Language" uiLanguage: "User interface language" -groupInvited: "You've been invited to a group" aboutX: "About {x}" emojiStyle: "Emoji style" native: "Native" disableDrawer: "Don't use drawer-style menus" -youHaveNoGroups: "You have no groups" -joinOrCreateGroup: "Get invited to a group or create your own." +showNoteActionsOnlyHover: "Only show note actions on hover" noHistory: "No history available" signinHistory: "Login history" -disableAnimatedMfm: "Disable MFM with animation" +enableAdvancedMfm: "Enable advanced MFM" +enableAnimatedMfm: "Enable animated MFM" doing: "Processing..." category: "Category" tags: "Tags" @@ -784,6 +780,7 @@ popularPosts: "Popular posts" shareWithNote: "Share with note" ads: "Advertisements" expiration: "Deadline" +startingperiod: "Start" memo: "Memo" priority: "Priority" high: "High" @@ -816,6 +813,7 @@ lastCommunication: "Last communication" resolved: "Resolved" unresolved: "Unresolved" breakFollow: "Remove follower" +breakFollowConfirm: "Really remove this follower?" itsOn: "Enabled" itsOff: "Disabled" emailRequiredForSignup: "Require email address for sign-up" @@ -835,8 +833,6 @@ deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" incorrectPassword: "Incorrect password." voteConfirm: "Confirm your vote for \"{choice}\"?" hide: "Hide" -leaveGroup: "Leave group" -leaveGroupConfirm: "Are you sure you want to leave \"{name}\"?" useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile" welcomeBackWithName: "Welcome back, {name}" clickToFinishEmailVerification: "Please click [{ok}] to complete email verification." @@ -852,16 +848,20 @@ instanceDefaultLightTheme: "Instance-wide default light theme" instanceDefaultDarkTheme: "Instance-wide default dark theme" instanceDefaultThemeDescription: "Enter the theme code in object format." mutePeriod: "Mute duration" +period: "Time limit" indefinitely: "Permanently" tenMinutes: "10 minutes" oneHour: "One hour" oneDay: "One day" oneWeek: "One week" +oneMonth: "One month" reflectMayTakeTime: "It may take some time for this to be reflected." failedToFetchAccountInformation: "Could not fetch account information" rateLimitExceeded: "Rate limit exceeded" cropImage: "Crop image" cropImageAsk: "Do you want to crop this image?" +cropYes: "Crop" +cropNo: "Use as-is" file: "File" recentNHours: "Last {n} hours" recentNDays: "Last {n} days" @@ -940,6 +940,21 @@ cannotPerformTemporaryDescription: "This action cannot be performed temporarily preset: "Preset" selectFromPresets: "Choose from presets" achievements: "Achievements" +gotInvalidResponseError: "Invalid server response" +gotInvalidResponseErrorDescription: "The server may be unreachable or undergoing maintenance. Please try again later." +thisPostMayBeAnnoying: "This note may annoy others." +thisPostMayBeAnnoyingHome: "Post to home timeline" +thisPostMayBeAnnoyingCancel: "Cancel" +thisPostMayBeAnnoyingIgnore: "Post anyway" +collapseRenotes: "Collapse renotes you've already seen" +internalServerError: "Internal Server Error" +internalServerErrorDescription: "The server has run into an unexpected error." +copyErrorInfo: "Copy error details" +joinThisServer: "Sign up at this instance" +exploreOtherServers: "Look for another instance" +letsLookAtTimeline: "Have a look at the timeline" +disableFederationWarn: "This will disable federation, but posts will continue to be public unless set otherwise. You usually do not need to use this setting." +invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up." _achievements: earnedAt: "Unlocked at" _types: @@ -1148,7 +1163,7 @@ _achievements: description: "You've clicked here" _justPlainLucky: title: "Just Plain Lucky" - description: "Has a chance to be obtained with a probability of 0.01% every 10 seconds" + description: "Has a chance to be obtained with a probability of 0.005% every 10 seconds" _setNameToSyuilo: title: "God Complex" description: "Set your name to \"syuilo\"" @@ -1167,7 +1182,7 @@ _achievements: _loggedInOnNewYearsDay: title: "Happy New Year!" description: "Logged in on the first day of the year" - flavor: "To another great year on this instance" + flavor: "To another great year!" _cookieClicked: title: "A game in which you click cookies" description: "Clicked the cookie" @@ -1324,72 +1339,6 @@ _nsfw: respect: "Hide NSFW media" ignore: "Don't hide NSFW media" force: "Hide all media" -_mfm: - cheatSheet: "MFM Cheatsheet" - intro: "MFM is a Misskey-exclusive markup language that can be used in many places. Here you can view a list of all available MFM syntax." - dummy: "Misskey expands the world of the Fediverse" - mention: "Mention" - mentionDescription: "You can specify a user by using an At-Symbol and a username." - hashtag: "Hashtag" - hashtagDescription: "You can specify a hashtag using a number sign and text." - url: "URL" - urlDescription: "URLs can be displayed." - link: "Link" - linkDescription: "Specific parts of text can be displayed as a URL." - bold: "Bold" - boldDescription: "Highlights letters by making them thicker." - small: "Small" - smallDescription: "Displays content small and thin." - center: "Center" - centerDescription: "Displays content centered." - inlineCode: "Code (Inline)" - inlineCodeDescription: "Displays inline syntax highlighting for (program) code." - blockCode: "Code (Block)" - blockCodeDescription: "Displays syntax highlighting for multi-line (program) code in a block." - inlineMath: "Math (Inline)" - inlineMathDescription: "Display math formulas (KaTeX) in-line" - blockMath: "Math (Block)" - blockMathDescription: "Display multi-line math formulas (KaTeX) in a block" - quote: "Quote" - quoteDescription: "Displays content as a quote." - emoji: "Custom Emoji" - emojiDescription: "By surrounding a custom emoji name with colons, custom emoji can be displayed." - search: "Search" - searchDescription: "Displays a search box with pre-entered text." - flip: "Flip" - flipDescription: "Flips content horizontally or vertically." - jelly: "Animation (Jelly)" - jellyDescription: "Gives content a jelly-like animation." - tada: "Animation (Tada)" - tadaDescription: "Gives content a \"Tada!\"-like animation." - jump: "Animation (Jump)" - jumpDescription: "Gives content a jumping animation." - bounce: "Animation (Bounce)" - bounceDescription: "Gives content a bouncy animation." - shake: "Animation (Shake)" - shakeDescription: "Gives content a shaking animation." - twitch: "Animation (Twitch)" - twitchDescription: "Gives content a strongly twitching animation." - spin: "Animation (Spin)" - spinDescription: "Gives content a spinning animation." - x2: "Big" - x2Description: "Displays content bigger." - x3: "Very big" - x3Description: "Displays content even bigger." - x4: "Unbelievably big" - x4Description: "Displays content even bigger than bigger than big." - blur: "Blur" - blurDescription: "Blurs content. It will be displayed clearly when hovered over." - font: "Font" - fontDescription: "Sets the font to display content in." - rainbow: "Rainbow" - rainbowDescription: "Makes the content appear in rainbow colors." - sparkle: "Sparkle" - sparkleDescription: "Gives content a sparkling particle effect." - rotate: "Rotate" - rotateDescription: "Turns content by a specified angle." - plain: "Plain" - plainDescription: "Deactivates the effects of all MFM contained within this MFM effect." _instanceTicker: none: "Never show" remote: "Show for remote users" @@ -1519,6 +1468,7 @@ _ago: weeksAgo: "{n}w ago" monthsAgo: "{n}mo ago" yearsAgo: "{n}y ago" + invalid: "None" _time: second: "Second(s)" minute: "Minute(s)" @@ -1552,14 +1502,29 @@ _tutorial: step8_3: "You can always change this setting later." _2fa: alreadyRegistered: "You have already registered a 2-factor authentication device." - registerDevice: "Register a new device" - registerKey: "Register a security key" + registerTOTP: "Register authenticator app" + passwordToTOTP: "Enter your password" step1: "First, install an authentication app (such as {a} or {b}) on your device." step2: "Then, scan the QR code displayed on this screen." + step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app." step2Url: "You can also enter this URL if you're using a desktop program:" + step3Title: "Enter an authentication code" step3: "Enter the token provided by your app to finish setup." step4: "From now on, any future login attempts will ask for such a login token." + securityKeyNotSupported: "Your browser does not support security keys." + registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key." securityKeyInfo: "Besides fingerprint or PIN authentication, you can also setup authentication via hardware security keys that support FIDO2 to further secure your account." + chromePasskeyNotSupported: "Chrome passkeys are currently not supported." + registerSecurityKey: "Register a security or pass key" + securityKeyName: "Enter a key name" + tapSecurityKey: "Please follow your browser to register the security or pass key" + removeKey: "Remove security key" + removeKeyConfirm: "Really delete the {name} key?" + whyTOTPOnlyRenew: "The authenticator app cannot be removed as long as a security key is registered." + renewTOTP: "Reconfigure authenticator app" + renewTOTPConfirm: "This will cause verification codes from your previous app to stop working" + renewTOTPOk: "Reconfigure" + renewTOTPCancel: "Cancel" _permissions: "read:account": "View your account information" "write:account": "Edit your account information" @@ -1594,18 +1559,20 @@ _permissions: "read:gallery-likes": "View your list of liked gallery posts" "write:gallery-likes": "Edit your list of liked gallery posts" _auth: + shareAccessTitle: "Granting application permissions" shareAccess: "Would you like to authorize \"{name}\" to access this account?" shareAccessAsk: "Are you sure you want to authorize this application to access your account?" + permission: "{name} requests the following permissions" permissionAsk: "This application requests the following permissions" pleaseGoBack: "Please go back to the application" callback: "Returning to the application" denied: "Access denied" + pleaseLogin: "Please log in to authorize applications." _antennaSources: all: "All notes" homeTimeline: "Notes from followed users" users: "Notes from specific users" userList: "Notes from a specified list of users" - userGroup: "Notes from users in a specified group" _weekday: sunday: "Sunday" monday: "Monday" @@ -1680,8 +1647,8 @@ _visibility: followersDescription: "Make visible to your followers only" specified: "Direct" specifiedDescription: "Make visible for specified users only" - localOnly: "Local only" - localOnlyDescription: "Not visible to remote users" + disableFederation: "Unfederated" + disableFederationDescription: "Don't transmit to other instances" _postForm: replyPlaceholder: "Reply to this note..." quotePlaceholder: "Quote this note..." @@ -1819,12 +1786,9 @@ _notification: youGotReply: "{name} replied to you" youGotQuote: "{name} quoted you" youRenoted: "Renote from {name}" - youGotMessagingMessageFromUser: "{name} sent you a chat message" - youGotMessagingMessageFromGroup: "A chat message was sent to the {name} group" youWereFollowed: "followed you" youReceivedFollowRequest: "You've received a follow request" yourFollowRequestAccepted: "Your follow request was accepted" - youWereInvitedToGroup: "{userName} invited you to a group" pollEnded: "Poll results have become available" unreadAntennaNote: "Antenna {name}" emptyPushNotificationMessage: "Push notifications have been updated" @@ -1840,7 +1804,7 @@ _notification: pollEnded: "Polls ending" receiveFollowRequest: "Received follow requests" followRequestAccepted: "Accepted follow requests" - groupInvited: "Group invitations" + achievementEarned: "Achievement unlocked" app: "Notifications from linked apps" _actions: followBack: "followed you back" @@ -1873,3 +1837,6 @@ _deck: channel: "Channel" mentions: "Mentions" direct: "Direct notes" +_dialog: + charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}." + charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index f3cd85e6a0..3ad2c21ff7 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -56,7 +56,7 @@ reply: "Responder" loadMore: "Ver más" showMore: "Ver más" showLess: "Cerrar" -youGotNewFollower: "te ha seguido" +youGotNewFollower: "ahora te sigue" receiveFollowRequest: "Recibiste una solicitud de seguimiento" followRequestAccepted: "La solicitud de seguimiento fue aceptada" mention: "Menciones" @@ -103,6 +103,8 @@ renoted: "Renotado" cantRenote: "No se puede renotar este post" cantReRenote: "No se puede renotar una renota" quote: "Citar" +inChannelRenote: "Renota sólo del canal" +inChannelQuote: "Cita sólo del canal" pinnedNote: "Nota fijada" pinned: "Fijar al perfil" you: "Tú" @@ -257,6 +259,8 @@ noMoreHistory: "El historial se ha acabado" startMessaging: "Iniciar chat" nUsersRead: "Leído por {n} personas" agreeTo: "De acuerdo con {0}" +agreeBelow: "Estoy de acuerdo con lo siguiente" +basicNotesBeforeCreateAccount: "Notas básicas" tos: "Términos de uso" start: "Comenzar" home: "Inicio" @@ -389,16 +393,19 @@ about: "Información" aboutMisskey: "Sobre Misskey" administrator: "Administrador" token: "Token" -twoStepAuthentication: "Autenticación de dos factores" +2fa: "Autenticación de doble factor" +totp: "Aplicación autentícadora" +totpDescription: "Ingresa una contaseña de un sólo uso usando la aplicación autenticadora" moderator: "Moderador" moderation: "Moderación" nUsersMentioned: "{n} usuarios mencionados" +securityKeyAndPasskey: "Clave de seguridad / clave de paso" securityKey: "Clave de seguridad" -securityKeyName: "Nombre de la Clave" -registerSecurityKey: "Registrar clave de seguridad" lastUsed: "Última vez usado" +lastUsedAt: "Último uso: {t}" unregister: "Cancelar registro" passwordLessLogin: "Iniciar sesión sin contraseña" +passwordLessLoginDescription: "Iniciar sesión con sólo una clave se seguridad / de paso sin usar una contraseña" resetPassword: "Resetear contraseña" newPasswordIs: "La nueva contraseña es \"{password}\"" reduceUiAnimation: "Reducir la animación de la UI" @@ -413,24 +420,15 @@ markAsReadAllTalkMessages: "Marcar todos los chats como leídos" help: "Ayuda" inputMessageHere: "Escribe el mensaje aquí" close: "Cerrar" -group: "Grupo" -groups: "Grupos" -createGroup: "Crear grupo" -ownedGroups: "Tus" -joinedGroups: "Grupos a los que me uní" invites: "Invitar" -groupName: "Nombre del grupo" members: "Miembros" transfer: "Transferir" -messagingWithUser: "Chatear con usuario" -messagingWithGroup: "Chatear en grupo" title: "Título" text: "Texto" enable: "Activar" next: "Siguiente" retype: "Intentar de nuevo" noteOf: "Notas de {user}" -inviteToGroup: "Invitar al grupo" quoteAttached: "Cita añadida" quoteQuestion: "¿Quiere añadir una cita?" noMessagesYet: "Aún no hay chat" @@ -452,20 +450,18 @@ passwordMatched: "Correcto" passwordNotMatched: "Las contraseñas no son las mismas" signinWith: "Inicie sesión con {x}" signinFailed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario y contraseña correctos." -tapSecurityKey: "Toque la clave de seguridad" or: "O" language: "Idioma" uiLanguage: "Idioma de visualización de la interfaz" -groupInvited: "Invitado al grupo" aboutX: "Acerca de {x}" emojiStyle: "Estilo de emoji" native: "Nativo" disableDrawer: "No mostrar los menús en cajones" -youHaveNoGroups: "Sin grupos" -joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su propio grupo." +showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor" noHistory: "No hay datos en el historial" signinHistory: "Historial de ingresos" -disableAnimatedMfm: "Deshabilitar MFM que tiene animaciones" +enableAdvancedMfm: "Habilitar MFM avanzado" +enableAnimatedMfm: "Habilitar MFM con movimiento" doing: "Voy en camino" category: "Categoría" tags: "Etiqueta" @@ -784,6 +780,7 @@ popularPosts: "Más vistos" shareWithNote: "Compartir con una nota" ads: "Anuncios" expiration: "Termina el" +startingperiod: "periodo de inicio" memo: "Notas" priority: "Prioridad" high: "Alta" @@ -816,6 +813,7 @@ lastCommunication: "Última comunicación" resolved: "Resuelto" unresolved: "Sin resolver" breakFollow: "Dejar de seguir" +breakFollowConfirm: "¿Quieres dejar de seguir?" itsOn: "¡Está encendido!" itsOff: "¡Está apagado!" emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro de la cuenta" @@ -835,8 +833,6 @@ deleteAccountConfirm: "La cuenta será borrada. ¿Está seguro?" incorrectPassword: "La contraseña es incorrecta" voteConfirm: "¿Confirma su voto a {choice}?" hide: "Ocultar" -leaveGroup: "Dejar el grupo" -leaveGroupConfirm: "¿Desea salir de {name}?" useDrawerReactionPickerForMobile: "Mostrar panel de reacciones en móviles" welcomeBackWithName: "Bienvenido otra vez, {name}" clickToFinishEmailVerification: "Cliquée {ok} y verifique su correo" @@ -852,16 +848,20 @@ instanceDefaultLightTheme: "Tema claro por defecto de la instancia" instanceDefaultDarkTheme: "Tema oscuro por defecto de la instancia" instanceDefaultThemeDescription: "Ingrese el código del tema en formato objeto" mutePeriod: "Período de silenciamiento" +period: "Termina el" indefinitely: "Sin límite de tiempo" tenMinutes: "10 minutos" oneHour: "1 hora" oneDay: "1 día" oneWeek: "1 semana" +oneMonth: "1 mes" reflectMayTakeTime: "Puede pasar un tiempo hasta que se reflejen los cambios" failedToFetchAccountInformation: "No se pudo obtener información de la cuenta" rateLimitExceeded: "Se excedió el límite de peticiones" cropImage: "Recortar imágen" cropImageAsk: "¿Desea recortar la imagen?" +cropYes: "Recortar" +cropNo: "Usar como está" file: "Archivos" recentNHours: "Últimas {n} horas" recentNDays: "Últimos {n} días" @@ -940,6 +940,21 @@ cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se preset: "Predefinido" selectFromPresets: "Escoger desde predefinidos" achievements: "Logros" +gotInvalidResponseError: "Respuesta del servidor inválida" +gotInvalidResponseErrorDescription: "Puede que el servidor esté caído o en mantenimiento. Favor de intentar más tarde" +thisPostMayBeAnnoying: "Ésta publicación puede resultar molesta." +thisPostMayBeAnnoyingHome: "Publicar en línea de tiempo 'Inicio'" +thisPostMayBeAnnoyingCancel: "detener" +thisPostMayBeAnnoyingIgnore: "Publicar de todos modos" +collapseRenotes: "Colapsar renotas que ya hayas visto" +internalServerError: "Error interno del servidor" +internalServerErrorDescription: "El servidor tuvo un error inesperado." +copyErrorInfo: "Copiar detalles del error" +joinThisServer: "Registrarse en esta instancia" +exploreOtherServers: "Buscar otra instancia" +letsLookAtTimeline: "Mirar la línea de tiempo local" +disableFederationWarn: "Esto desactivará la federación, pero las publicaciones segurán siendo públicas al menos que se configure diferente. Usualmente no necesitas usar esta configuración." +invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido." _achievements: earnedAt: "Desbloqueado el" _types: @@ -1324,72 +1339,6 @@ _nsfw: respect: "Ocultar medios NSFW" ignore: "No esconder medios NSFW " force: "Ocultar todos los medios" -_mfm: - cheatSheet: "Hoja de referencia de MFM" - intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de Misskey. Aquí puede ver una lista de sintaxis disponibles en MFM." - dummy: "Misskey expande el mundo de la Fediverso" - mention: "Menciones" - mentionDescription: "El signo @ seguido de un nombre de usuario se puede utilizar para notificar a un usuario en particular." - hashtag: "Hashtag" - hashtagDescription: "Puede especificar un hashtag con un numeral y el texto." - url: "URL" - urlDescription: "Se pueden mostrar las URL" - link: "Vínculo" - linkDescription: "Se pueden asociar partes de texto a la URL" - bold: "Negrita" - boldDescription: "Muestra el texto con las letras más gruesas" - small: "Pequeño" - smallDescription: "Muestra el texto más pequeño y delgado" - center: "Centrar" - centerDescription: "Muestra el texto centrado" - inlineCode: "Código (insertado)" - inlineCodeDescription: "Muestra el código de un programa resaltando su sintaxis" - blockCode: "Código (bloque)" - blockCodeDescription: "Código de resaltado de sintaxis, como programas de varias líneas con bloques." - inlineMath: "Fórmula (insertado)" - inlineMathDescription: "Muestra fórmulas (KaTeX) insertadas" - blockMath: "Fórmula (bloque)" - blockMathDescription: "Muestra fórmulas (KaTeX) de varias líneas en un bloque" - quote: "Citar" - quoteDescription: "Muestra el contenido como una cita" - emoji: "Emojis personalizados" - emojiDescription: "Muestra los emojis personalizados encerrados entre dos puntos." - search: "Buscar" - searchDescription: "Muestra una caja de búsqueda con texto pre-escrito" - flip: "Echar de un capirotazo" - flipDescription: "Voltea el contenido hacia arriba / abajo o hacia la izquierda / derecha." - jelly: "Animación (gelatina)" - jellyDescription: "Aplica un efecto de animación tipo gelatina" - tada: "Animación (tadá)" - tadaDescription: "Aplica un efecto de animación al estilo \"Tadá\"" - jump: "Animación (saltar)" - jumpDescription: "Aplica un efecto de animación tipo salto" - bounce: "Animación (rebotar)" - bounceDescription: "Aplica un efecto de animación tipo rebote" - shake: "Animación (temblor)" - shakeDescription: "Aplica un efecto de animación tipo temblor" - twitch: "Animación (sacudida)" - twitchDescription: "Aplica un efecto de animación tipo sacudida" - spin: "Animación (giro)" - spinDescription: "Aplica un efecto de animación tipo rotación" - x2: "Grande" - x2Description: "Muestra el contenido más grande" - x3: "Muy grande" - x3Description: "Muestra el contenido mucho más grande" - x4: "Totalmente grande" - x4Description: "Muestra el contenido totalmente grande" - blur: "Desenfoque" - blurDescription: "Para desenfocar el contenido. Se muestra claramente al colocar el puntero encima." - font: "Fuente" - fontDescription: "Elegir la fuente del contenido" - rainbow: "Arcoíris" - rainbowDescription: "Muestra el contenido con los colores del arcoíris" - sparkle: "Parpadeante" - sparkleDescription: "Aplica un efecto de partículas parpadeantes" - rotate: "Rotar" - rotateDescription: "Rota el contenido a un ángulo especificado." - plain: "Plano" - plainDescription: "Desactiva los efectos de todo el contenido MFM con este efecto MFM." _instanceTicker: none: "No mostrar" remote: "Mostrar a usuarios remotos" @@ -1519,6 +1468,7 @@ _ago: weeksAgo: "Hace {n} semanas" monthsAgo: "Hace {n} meses" yearsAgo: "Hace {n} años" + invalid: "No hay nada que ver aqui" _time: second: "Segundos" minute: "Minutos" @@ -1552,14 +1502,29 @@ _tutorial: step8_3: "La configuración de las notificaciones puede modificarse posteriormente." _2fa: alreadyRegistered: "Ya has completado la configuración." - registerDevice: "Registrar dispositivo" - registerKey: "Registrar clave" + registerTOTP: "Registrar aplicación autenticadora" + passwordToTOTP: "Ingresa tu contraseña" step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra." step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla." + step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app.\nTocar este código QR te permitirá registrar la autenticación 2FA a tu llave de seguridad o aplicación autenticadora." step2Url: "En una aplicación de escritorio se puede ingresar la siguiente URL:" + step3Title: "Ingresa un código de autenticación" step3: "Para terminar, ingrese el token mostrado en la aplicación." step4: "Ahora cuando inicie sesión, ingrese el mismo token" + securityKeyNotSupported: "Tu navegador no soporta claves de autenticación." + registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key.\npor favor. configura una aplicación de autenticación para registrar una llave de seguridad." securityKeyInfo: "Se puede configurar el inicio de sesión usando una clave de seguridad de hardware que soporte FIDO2 o con un certificado de huella digital o con un PIN" + chromePasskeyNotSupported: "Las llaves de seguridad de Chrome no son soportadas por el momento." + registerSecurityKey: "Registrar una llave de seguridad" + securityKeyName: "Ingresa un nombre para la clave" + tapSecurityKey: "Por favor, sigue tu navegador para registrar una llave de seguridad" + removeKey: "Quitar la llave de seguridad" + removeKeyConfirm: "¿Borrar el respaldo \"{name}\"?" + whyTOTPOnlyRenew: "The authenticator app cannot be removed as long as a security key is registered.\nLa aplicación autenticadora no puede ser eliminada mientras la llave de seguridad se encuentre registrada." + renewTOTP: "Reconfigurar la aplicación autenticadora" + renewTOTPConfirm: "This will cause verification codes from your previous app to stop working\nEsto hará que los códigos de verificación de la aplicación anterior dejen de funcionar" + renewTOTPOk: "Reconfigurar" + renewTOTPCancel: "No gracias" _permissions: "read:account": "Ver información de la cuenta" "write:account": "Editar información de la cuenta" @@ -1594,18 +1559,20 @@ _permissions: "read:gallery-likes": "Ver favoritos de la galería" "write:gallery-likes": "Editar favoritos de la galería" _auth: + shareAccessTitle: "Permisos de la aplicación" shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?" shareAccessAsk: "¿Está seguro de que desea autorizar esta aplicación para acceder a su cuenta?" + permission: "{name} solicita los siguientes permisos" permissionAsk: "Esta aplicación requiere los siguientes permisos" pleaseGoBack: "Por favor, vuelve a la aplicación" callback: "Volviendo a la aplicación" denied: "Acceso denegado" + pleaseLogin: "Se requiere un inicio de sesión para darle permisos a la aplicación" _antennaSources: all: "Todas las notas" homeTimeline: "Notas de los usuarios que sigues" users: "Notas de un usuario o varios" userList: "Notas de los usuarios de una lista" - userGroup: "Notas de los usuarios de una grupo" _weekday: sunday: "Domingo" monday: "Lunes" @@ -1680,8 +1647,8 @@ _visibility: followersDescription: "Visible sólo para tus seguidores" specified: "Mensaje directo" specifiedDescription: "Visible sólo para los usuarios elegidos" - localOnly: "Solo local" - localOnlyDescription: "Oculto para usuarios remotos" + disableFederation: "No federado" + disableFederationDescription: "No enviar a otras instancias" _postForm: replyPlaceholder: "Responder a esta nota" quotePlaceholder: "Citar esta nota" @@ -1819,12 +1786,9 @@ _notification: youGotReply: "Respuesta de {name}" youGotQuote: "Citado por {name}" youRenoted: "Renotado por {name}" - youGotMessagingMessageFromUser: "{name} comenzó un chat contigo" - youGotMessagingMessageFromGroup: "Tienes un chat de {name}" youWereFollowed: "te ha seguido" youReceivedFollowRequest: "Has mandado una solicitud de seguimiento" yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada" - youWereInvitedToGroup: "Invitado al grupo" pollEnded: "Estan disponibles los resultados de la encuesta" unreadAntennaNote: "Antena {name}" emptyPushNotificationMessage: "Se han actualizado las notificaciones push" @@ -1840,7 +1804,7 @@ _notification: pollEnded: "La encuesta terminó" receiveFollowRequest: "Recibió una solicitud de seguimiento" followRequestAccepted: "El seguimiento fue aceptado" - groupInvited: "Invitado al grupo" + achievementEarned: "Logro desbloqueado" app: "Notificaciones desde aplicaciones" _actions: followBack: "Te sigue de vuelta" @@ -1873,3 +1837,6 @@ _deck: channel: "Canal" mentions: "Menciones" direct: "Mensaje directo" +_dialog: + charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}." + charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}." diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 8a9476e916..f9b8939e8b 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -103,6 +103,8 @@ renoted: "Renoté !" cantRenote: "Ce message ne peut pas être renoté." cantReRenote: "Impossible de renoter une Renote." quote: "Citer" +inChannelRenote: "Renoter dans le canal" +inChannelQuote: "Citer dans le canal" pinnedNote: "Note épinglée" pinned: "Épingler sur le profil" you: "Vous" @@ -129,6 +131,7 @@ unblockConfirm: "Êtes-vous sûr·e de vouloir débloquer ce compte ?" suspendConfirm: "Êtes-vous sûr·e de vouloir suspendre ce compte ?" unsuspendConfirm: "Êtes-vous sûr·e de vouloir annuler la suspension de ce compte ?" selectList: "Sélectionner une liste" +selectChannel: "Sélectionner un canal" selectAntenna: "Sélectionner une antenne" selectWidget: "Sélectionner un widget" editWidgets: "Modifier les widgets" @@ -388,13 +391,10 @@ about: "Informations" aboutMisskey: "À propos de Misskey" administrator: "Administrateur" token: "Jeton" -twoStepAuthentication: "Authentification à deux facteurs" moderator: "Modérateur·rice·s" moderation: "Modérations" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" securityKey: "Clé de sécurité" -securityKeyName: "Nom de la clé" -registerSecurityKey: "Enregistrer une clé de sécurité" lastUsed: "Dernier utilisé" unregister: "Se désinscrire" passwordLessLogin: "Se connecter sans mot de passe" @@ -412,24 +412,15 @@ markAsReadAllTalkMessages: "Marquer toutes les discussions comme lues" help: "Aide" inputMessageHere: "Écrivez votre message ici" close: "Fermer" -group: "Groupe" -groups: "Groupes" -createGroup: "Créer un groupe" -ownedGroups: "Mes groupes" -joinedGroups: "Groupes rejoints" invites: "Invitations" -groupName: "Nom du groupe" members: "Membres" transfer: "Transférer" -messagingWithUser: "Discuter avec un·e autre utilisateur·rice" -messagingWithGroup: "Discuter avec un groupe" title: "Titre" text: "Texte" enable: "Activer" next: "Suivant" retype: "Confirmation" noteOf: "Notes de {user}" -inviteToGroup: "Inviter dans un groupe" quoteAttached: "Avec citation" quoteQuestion: "Souhaitez-vous ajouter une citation ?" noMessagesYet: "Pas encore de discussion" @@ -451,20 +442,15 @@ passwordMatched: "Les mots de passe correspondent" passwordNotMatched: "Les mots de passe ne correspondent pas" signinWith: "Se connecter avec {x}" signinFailed: "Échec d’authentification. Veuillez vérifier que votre nom d’utilisateur et mot de passe sont corrects." -tapSecurityKey: "Appuyez sur votre clé de sécurité" or: "OU" language: "Langue" uiLanguage: "Langue d’affichage de l’interface" -groupInvited: "Invité au groupe" aboutX: "À propos de {x}" emojiStyle: "Style des émojis" native: "Natif" disableDrawer: "Les menus ne s'affichent pas dans le tiroir" -youHaveNoGroups: "Vous n’avez aucun groupe" -joinOrCreateGroup: "Vous pouvez être invité·e à rejoindre des groupes existants ou créer votre propre nouveau groupe." noHistory: "Pas d'historique" signinHistory: "Historique de connexion" -disableAnimatedMfm: "Désactiver MFM ayant des animations" doing: "En cours..." category: "Catégorie" tags: "Étiquettes" @@ -541,7 +527,7 @@ updateRemoteUser: "Mettre à jour les informations de l’utilisateur·rice dist deleteAllFiles: "Supprimer tous les fichiers" deleteAllFilesConfirm: "Êtes-vous sûr·e de vouloir supprimer tous les fichiers ?" removeAllFollowing: "Retenir tous les abonnements" -removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Veuillez lancer cette action uniquement si l’instance n’existe plus." +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." yourAccountSuspendedTitle: "Ce compte est suspendu" @@ -832,8 +818,6 @@ deleteAccountConfirm: "Votre compte sera supprimé. Êtes vous certain ?" incorrectPassword: "Le mot de passe est incorrect." voteConfirm: "Confirmez-vous votre vote pour « {choice} » ?" hide: "Masquer" -leaveGroup: "Quitter le groupe" -leaveGroupConfirm: "Êtes vous sûr de vouloir quitter \"{name}\" ?" useDrawerReactionPickerForMobile: "Afficher le sélecteur de réactions en tant que panneau sur mobile" welcomeBackWithName: "Heureux de vous revoir, {name}" clickToFinishEmailVerification: "Veuillez cliquer sur [{ok}] afin de compléter la vérification par courriel." @@ -849,6 +833,7 @@ instanceDefaultLightTheme: "Thème clair par défaut sur toute l’instance" instanceDefaultDarkTheme: "Thème sombre par défaut sur toute l’instance" instanceDefaultThemeDescription: "Saisissez le code du thème en format objet." mutePeriod: "Durée de mise en sourdine" +period: "Fin du sondage" indefinitely: "Illimité" tenMinutes: "10 minutes" oneHour: "1 heure" @@ -917,6 +902,17 @@ show: "Affichage" neverShow: "Ne plus afficher" remindMeLater: "Peut-être plus tard" color: "Couleur" +_achievements: + _types: + _notes100000: + title: "ALL YOUR NOTE ARE BELONG TO US" + _login1000: + flavor: "Merci d'utiliser Misskey !" + _markedAsCat: + title: "Je suis un chat" + flavor: "Je n'ai pas encore de nom" + _following50: + title: "Beaucoup d'amis" _role: priority: "Priorité" _priority: @@ -1011,72 +1007,6 @@ _nsfw: respect: "Cacher les médias marqués comme contenu sensible" ignore: "Afficher les médias sensibles" force: "Cacher tous les médias" -_mfm: - cheatSheet: "Antisèche MFM" - intro: "MFM est un langage Markdown spécifique utilisable ici et là dans Misskey. Vous pouvez vérifier ici les structures utilisables avec MFM." - dummy: "La Fédiverse s'agrandit avec Misskey" - mention: "Mentionner" - mentionDescription: "Vous pouvez afficher un utilisateur spécifique en indiquant une arobase suivie d'un nom d'utilisateur" - hashtag: "Hashtags" - hashtagDescription: "Vous pouvez afficher un mot-dièse en utilisant un croisillon et du texte" - url: "URL" - urlDescription: "L'adresse web peut être affichée." - link: "Lien" - linkDescription: "Une partie précise d'une phrase peut être liée à l'adresse web." - bold: "Gras" - boldDescription: "Il est possible de mettre le texte en exergue en le mettant en gras." - small: "Diminuer l'emphase" - smallDescription: "Le contenu peut être affiché en petit et fin." - center: "Centrer" - centerDescription: "Le contenu peut être centré" - inlineCode: "Code (inline)" - inlineCodeDescription: "Coloration syntaxique des lignes de code." - blockCode: "Bloc de code" - blockCodeDescription: "Coloration syntaxique des lignes de code pour les blocs multi-lignes." - inlineMath: "Formule mathématique (inline)" - inlineMathDescription: "Afficher les formules mathématiques (KaTeX)." - blockMath: "Formule mathématique (bloc)" - blockMathDescription: "Afficher les formules mathématiques (KaTeX) multi-lignes dans un bloc." - quote: "Citer" - quoteDescription: "Affiche le contenu sous forme de citation." - emoji: "Émojis personnalisés" - emojiDescription: "Entourez le nom de l'émoji personnalisé de deux points pour l'afficher." - search: "Rechercher" - searchDescription: "Affiche une boîte de recherche avec du texte pré-saisi." - flip: "Inverser" - flipDescription: "Rotation verticale ou horizontale du contenu" - jelly: "Animation (Gelée)" - jellyDescription: "Donne une animation d'étirement." - tada: "Animation (Tada)" - tadaDescription: "Donne une animation qui donne une impression de \"Tada !\"" - jump: "Animation (Saut)" - jumpDescription: "Donne une animation qui saute." - bounce: "Animation (Rebond)" - bounceDescription: "Donne une animation de rebondissement." - shake: "Animation (Secousse)" - shakeDescription: "Donne une animation tremblante." - twitch: "Animation (Tremblement)" - twitchDescription: "Donne une animation de tremblement intense." - spin: "Animation (Rotation)" - spinDescription: "Donne une animation de rotation." - x2: "Grand" - x2Description: "Afficher le contenu en grand." - x3: "Très grand" - x3Description: "Afficher le contenu en très grand." - x4: "Plus grand" - x4Description: "Afficher le contenu en plus grand." - blur: "Flou" - blurDescription: "Le contenu peut être flouté ; il sera visible en le survolant avec le curseur." - font: "Police de caractères" - fontDescription: "Il est possible de choisir la police." - rainbow: "Arc-en-ciel" - rainbowDescription: "Permet d'afficher le contenu en couleurs arc-en-ciel." - sparkle: "Paillettes" - sparkleDescription: "Ajoute un effet scintillant au contenu." - rotate: "Pivoter" - rotateDescription: "Faire pivoter à un angle spécifié." - plain: "Vu texte non formaté" - plainDescription: "Désactive toute la syntaxe interne." _instanceTicker: none: "Cacher " remote: "Montrer pour les utilisateur·ice·s distant·e·s" @@ -1206,6 +1136,7 @@ _ago: weeksAgo: "Il y a {n} semaines" monthsAgo: "Il y a {n} mois" yearsAgo: "Il y a {n} ans" + invalid: "Il n'y a rien à voir ici" _time: second: "s" minute: "min" @@ -1238,14 +1169,14 @@ _tutorial: step8_2: "En les activant, vous recevrez des notifications pour les mentions, les réactions, les suivis, etc., même lorsque Misskey n'est pas ouvert." _2fa: alreadyRegistered: "Configuration déjà achevée." - registerDevice: "Ajouter un nouvel appareil" - registerKey: "Enregistrer une clef" step1: "Tout d'abord, installez une application d'authentification, telle que {a} ou {b}, sur votre appareil." step2: "Ensuite, scannez le code QR affiché sur l’écran." step2Url: "Vous pouvez également saisir cette URL si vous utilisez un programme de bureau :" step3: "Entrez le jeton affiché sur votre application pour compléter la configuration." step4: "À partir de maintenant, ce même jeton vous sera demandé à chacune de vos connexions." 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." + removeKeyConfirm: "Voulez-vous supprimer {name} ?" + renewTOTPCancel: "Pas maintenant" _permissions: "read:account": "Afficher les informations du compte" "write:account": "Mettre à jour les informations de votre compte" @@ -1291,7 +1222,6 @@ _antennaSources: homeTimeline: "Notes venant des utilisateur·rice·s auxquel·les je suis abonné" users: "Notes venant de la part d’utilisateur·rice·s précis" userList: "Notes venant d’une liste spécifique" - userGroup: "Notes venant d’utilisateur·rice·s du groupe spécifié" _weekday: sunday: "Dimanche" monday: "Lundi" @@ -1363,8 +1293,6 @@ _visibility: followersDescription: "Publier à vos abonné·e·s uniquement" specified: "Direct" specifiedDescription: "Publier uniquement aux utilisateur·rice·s mentionné·e·s" - localOnly: "Local seulement" - localOnlyDescription: "Caché pour les utilisateurs distant" _postForm: replyPlaceholder: "Répondre à cette note ..." quotePlaceholder: "Citez cette note ..." @@ -1492,12 +1420,9 @@ _notification: youGotReply: "Réponse de {name}" youGotQuote: "Cité·e par {name}" youRenoted: "{name} vous a Renoté" - youGotMessagingMessageFromUser: "{name} vous envoyé un message" - youGotMessagingMessageFromGroup: "Un message a été envoyé au groupe {name}" youWereFollowed: "Vous suit" youReceivedFollowRequest: "Vous avez reçu une demande d’abonnement" yourFollowRequestAccepted: "Votre demande d’abonnement a été accepté" - youWereInvitedToGroup: "Invité·e au groupe" pollEnded: "Les résultats du sondage sont disponibles" unreadAntennaNote: "Antenne {name}" emptyPushNotificationMessage: "Les notifications push ont été mises à jour" @@ -1512,7 +1437,6 @@ _notification: pollEnded: "Sondages se cloturant" receiveFollowRequest: "Demande d'abonnement reçue" followRequestAccepted: "Demande d'abonnement acceptée" - groupInvited: "Invitation à un groupe" app: "Notifications provenant des apps" _actions: followBack: "Suivre" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 7332e030c4..5d74cf5389 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -84,7 +84,7 @@ error: "Galat" somethingHappened: "Terjadi kesalahan" retry: "Coba lagi" pageLoadError: "Gagal memuat halaman." -pageLoadErrorDescription: "Umumnya disebabkan jaringan atau tembolok perambah. Cobalah bersihkan tembolok peramban lalu tunggu sesaat sebelum mencoba kembali." +pageLoadErrorDescription: "Umumnya disebabkan jaringan atau tembolok peramban. Cobalah bersihkan tembolok peramban lalu tunggu sesaat sebelum mencoba kembali." serverIsDead: "Tidak ada respon dari peladen. Mohon tunggu dan coba beberapa saat lagi." youShouldUpgradeClient: "Untuk melihat halaman ini, mohon muat ulang untuk memutakhirkan klienmu." enterListName: "Masukkan nama daftar" @@ -103,6 +103,8 @@ renoted: "Telah direnote" cantRenote: "Postingan ini tidak dapat direnote" cantReRenote: "Renote tidak dapat direnote" quote: "Kutip" +inChannelRenote: "Hanya renote dalam kanal" +inChannelQuote: "Hanya kutip dalam kanal" pinnedNote: "Catatan yang disematkan" pinned: "Sematkan ke profil" you: "Kamu" @@ -129,6 +131,7 @@ unblockConfirm: "Apakah kamu yakin ingin membuka blokir akun ini?" suspendConfirm: "Apakah kamu yakin ingin membekukan akun ini?" unsuspendConfirm: "Apakah kamu yakin ingin membuka pembekuan akun ini?" selectList: "Pilih daftar" +selectChannel: "Pilih kanal" selectAntenna: "Pilih Antena" selectWidget: "Pilih gawit" editWidgets: "Sunting gawit" @@ -256,6 +259,8 @@ noMoreHistory: "Tidak ada sejarah lagi" startMessaging: "Mulai mengirim pesan" nUsersRead: "Dibaca oleh {n}" agreeTo: "Saya setuju kepada {0}" +agreeBelow: "Saya setuju dengan di bawah ini" +basicNotesBeforeCreateAccount: "Catatan penting" tos: "Syarat dan ketentuan" start: "Mulai" home: "Beranda" @@ -388,13 +393,10 @@ about: "Informasi" aboutMisskey: "Tentang Misskey" administrator: "Admin" token: "Token" -twoStepAuthentication: "Otentikasi dua faktor" moderator: "Moderator" moderation: "Moderasi" nUsersMentioned: "{n} pengguna disebut" securityKey: "Kunci keamanan" -securityKeyName: "Nama kunci" -registerSecurityKey: "Daftarkan kunci keamanan" lastUsed: "Terakhir digunakan" unregister: "Batalkan pendaftaran" passwordLessLogin: "Setel login tanpa kata sandi" @@ -412,24 +414,15 @@ markAsReadAllTalkMessages: "Tandai semua pesan telah dibaca" help: "Bantuan" inputMessageHere: "Ketik pesan disini" close: "Tutup" -group: "Grup" -groups: "Grup" -createGroup: "Buat grup" -ownedGroups: "Grup yang dimiliki" -joinedGroups: "Grup yang diikuti" invites: "Undang" -groupName: "Nama grup" members: "Anggota" transfer: "Transfer" -messagingWithUser: "Obrolan dengan pengguna lain" -messagingWithGroup: "Obrolan di dalam grup" title: "Judul" text: "Teks" enable: "Aktifkan" next: "Selanjutnya" retype: "Masukkan ulang" noteOf: "Catatan milik {user}" -inviteToGroup: "Undang ke grup" quoteAttached: "Dikutip" quoteQuestion: "Apakah kamu ingin menambahkan kutipan?" noMessagesYet: "Tidak ada pesan" @@ -451,20 +444,17 @@ passwordMatched: "Kata sandi sama" passwordNotMatched: "Kata sandi tidak sama" signinWith: "Masuk dengan {x}" signinFailed: "Tidak dapat masuk. Nama pengguna atau kata sandi yang kamu masukkan salah." -tapSecurityKey: "Ketuk kunci keamanan kamu" or: "atau" language: "Bahasa" uiLanguage: "Bahasa antarmuka pengguna" -groupInvited: "Telah diundang ke grup" aboutX: "Tentang {x}" emojiStyle: "Gaya emoji" native: "Native" disableDrawer: "Jangan gunakan menu bergaya laci" -youHaveNoGroups: "Kamu tidak memiliki grup" -joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri." noHistory: "Tidak ada riwayat" signinHistory: "Riwayat masuk" -disableAnimatedMfm: "Nonaktifkan MFM dengan animasi" +enableAdvancedMfm: "Nyalakan MFM tingkat lanjut" +enableAnimatedMfm: "Nyalakan animasi MFM" doing: "Sedang berkerja..." category: "Kategori" tags: "Tandai" @@ -834,8 +824,6 @@ deleteAccountConfirm: "Akun akan dihapus. Apakah kamu yakin?" incorrectPassword: "Kata sandi salah." voteConfirm: "Konfirmasi suara kamu untuk ({choice})?" hide: "Sembunyikan" -leaveGroup: "Keluar grup" -leaveGroupConfirm: "Apakah kamu yakin untuk keluar dari \"{name}\"?" useDrawerReactionPickerForMobile: "Tampilkan bilah reaksi sebagai laci di ponsel" welcomeBackWithName: "Selamat datang kembali, {name}." clickToFinishEmailVerification: "Mohon klik [{ok}] untuk menyelesaikan verifikasi email." @@ -851,6 +839,7 @@ instanceDefaultLightTheme: "Bawaan instan tema terang" instanceDefaultDarkTheme: "Bawaan instan tema gelap" instanceDefaultThemeDescription: "Masukkan kode tema di format obyek." mutePeriod: "Batas waktu bisu" +period: "Batas akhir" indefinitely: "Selamanya" tenMinutes: "10 Menit" oneHour: "1 Jam" @@ -861,6 +850,8 @@ failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun" rateLimitExceeded: "Batas sudah terlampaui" cropImage: "potong gambar" cropImageAsk: "Ingin memotong gambar?" +cropYes: "Potong" +cropNo: "Gunakan apa adanya" file: "Berkas" recentNHours: "{n} jam terakhir" recentNDays: "{n} hari terakhir" @@ -927,10 +918,87 @@ didYouLikeMisskey: "Apakah kamu mulai menyukai Misskey?" pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu Misskey. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan Misskey tetap dapat berlanjut!" roles: "Peran" role: "Peran" +normalUser: "Pengguna umum" +undefined: "Tak terdefinisi" +assign: "Tetapkan\n" +unassign: "Batalkan penetapan" color: "Warna" +manageCustomEmojis: "Kelola Emoji Kustom" +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." +preset: "Prasetel" +selectFromPresets: "Pilih dari prasetel" +achievements: "Pencapaian" +gotInvalidResponseError: "Respon peladen tidak valid" +gotInvalidResponseErrorDescription: "Peladen tidak dapat dijangkau atau sedang dalam perawatan. Mohon coba lagi nanti." +thisPostMayBeAnnoying: "Catatan ini mungkin dapat mengganggu orang lain." +thisPostMayBeAnnoyingHome: "Catat ke linimasa beranda" +thisPostMayBeAnnoyingCancel: "Batalkan" +thisPostMayBeAnnoyingIgnore: "Tetap catat" +collapseRenotes: "Tutup renote yang sudah kamu lihat" +internalServerError: "Kesalahan internal peladen" +internalServerErrorDescription: "Peladen sedang mengalami galat tak terduga" +copyErrorInfo: "Salin detil galat" _achievements: + earnedAt: "Terbuka pada" _types: + _notes1: + title: "Cus, baru gabung Misskey nih!" + description: "Catat catatan pertama kamu" + flavor: "Selamat bersenang-senang dengan Misskey!" + _notes10: + title: "Beberapa catatan" + description: "Catat 10 catatan" + _notes100: + title: "Banyak catatan" + description: "Catat 100 catatan" + _notes500: + title: "Tertumpuk catatan" + description: "Catat 500 catatan" + _notes1000: + title: "Gunung catatan" + description: "Catat 1000 catatan" + _notes5000: + title: "Luapan catatan" + description: "Catat 5000 catatan" + _notes10000: + title: "Catatan super" + description: "Catat 10 ribu catatan" + _notes20000: + title: "Butuh... banyak... catatan..." + description: "Catat 20 ribu catatan" + _notes30000: + title: "Catat, catat, catat !" + description: "Catat 30 ribu catatan" + _notes40000: + title: "Pabrik catatan" + description: "Catat 40 ribu catatan" + _notes50000: + title: "Planet catatan" + description: "Catat 50 ribu catatan" + _notes60000: + title: "Kuasar catatan" + description: "Catat 60 ribu catatan" + _notes70000: + title: "Lubang hitam catatan" + description: "Catat 70 ribu catatan" + _notes80000: + title: "Galaksi catatan" + description: "Catat 80 ribu catatan" + _notes90000: + title: "Semesta catatan" + description: "Catat 90 ribu catatan" + _notes100000: + title: "ALL YOUR NOTE ARE BELONG TO US" + description: "Catat 100 ribu catatan" + flavor: "Banyak bacot ya kamu." + _login3: + title: "Pemula I" + description: "Login selama 3 hari" + flavor: "Mulai hari ini, panggil gue Misskist" _login7: + title: "Pemula II" description: "Login selama 7 hari" flavor: "Sudah mulai terbiasa?" _login15: @@ -1003,7 +1071,80 @@ _achievements: _following100: title: "100 Teman" description: "Ikuti 100 pengguna lain" + _following300: + title: "Kelebihan teman" + description: "Mengikuti 300 pengguna lain" + _followers1: + title: "Pengikut pertama" + description: "Dapatkan 1 pengikut" + _followers10: + title: "Ikuti aku!" + description: "Dapatkan 10 pengikut" + _followers50: + title: "Rame-rame" + description: "Dapatkan 50 pengikut" + _followers100: + title: "Terkenal" + description: "Dapatkan 100 pengikut" + _followers300: + title: "Mohon antri satu baris" + description: "Dapatkan 300 pengikut" + _followers500: + title: "Stasiun Informasi" + description: "Dapatkan 500 pengikut" + _followers1000: + title: "Influencer" + description: "Dapatkan 1000 pengikut" + _collectAchievements30: + title: "Kolektor pencapaian" + description: "Dapatkan 30 pencapaian" + _viewAchievements3min: + title: "Suka Pencapaian" + description: "Lugat daftar pencapaianmu setidaknya 3 menit" + _iLoveMisskey: + title: "I Love Misskey" + description: "Catat \"I ❤ #Misskey\"" + flavor: "Tim pengembang misskey sangat mengapresiasi dukungan kamu!" + _foundTreasure: + title: "Berburu Harta Karun" + description: "Kamu telah menemukan harta karun tersembunyi" + _client30min: + title: "Istirahat pendek" + description: "Habiskan waktu 30 menit di Misskey" + _noteDeletedWithin1min: + title: "Eh, salah coy!" + description: "Hapus catatan kurang dari semenit kamu catat" + _postedAtLateNight: + title: "Nokturnal" + description: "Catat catatan di tengah malam hari" + flavor: "Udah waktunya boboq." + _postedAt0min0sec: + title: "Jam ngomong" + description: "Catat catatan di jam 00.00" + flavor: "Tik Tok Tik Toeeeng" + _selfQuote: + title: "Rujukan mandiri" + description: "Kutip catatanmu sendiri" + _htl20npm: + title: "Linimasa mengalir" + description: "Memiliki linimasa beranda dengan kecepatan melebihi 20 cpm (catatan per menit)" + _viewInstanceChart: + title: "Analis" + description: "Lihat bagan instansimu" + _outputHelloWorldOnScratchpad: + title: "Halo, dunia!" + description: "Munculkan \"hello world\" di Scratchpad" + _open3windows: + title: "Jendela ganda" + description: "Memiliki setidaknya 3 jendela yang terbuka secara bersamaan" + _driveFolderCircularReference: + title: "Referensi Siklus" + description: "Mencoba membuat folder bersarang rekursif di Drive" + _reactWithoutRead: + title: "Beneran udah dibaca?" + description: "Mereaksi catatan dengan 100 karakter panjangnya dalam 3 detik setelah dicatat" _clickedClickHere: + title: "Klik di sini" description: "Kamu telah mengeklik disini" _justPlainLucky: title: "Lagi Beruntung" @@ -1026,6 +1167,7 @@ _achievements: _loggedInOnNewYearsDay: title: "Selamat Tahun Baru!" description: "Login di hari pertama tahun baru" + flavor: "Untuk tahun baru yang berkah bagi instansi ini" _cookieClicked: title: "Permainan dimana kamu mengeklik kue" description: "Mengeklik kue" @@ -1054,6 +1196,9 @@ _role: baseRole: "Templat peran" useBaseValue: "Gunakan nilai templat peran" chooseRoleToAssign: "Pilih peran yang ditugaskan" + iconUrl: "URL ikon" + asBadge: "Tampilkan sebagai lencana" + descriptionOfAsBadge: "Ikon peran ini akan ditampilkan bersebelahan dengan username pengguna yang memiliki peran ini jika dinyalakan." canEditMembersByModerator: "Perbolehkan moderator untuk menyunting daftar anggota untuk peran ini" descriptionOfCanEditMembersByModerator: "Ketika dinyalakan, moderator beserta administrator dapat menugaskan ataupun mencabut pengguna ke peran ini. Ketika dimatikan, hanya administrator saja yang dapat menugaskan pengguna ke peran ini." priority: "Prioritas" @@ -1069,6 +1214,36 @@ _role: canManageCustomEmojis: "Dapat mengelola Emoji kustom" driveCapacity: "Kapasitas Drive" pinMax: "Jumlah maksimal catatan yang disematkan" + antennaMax: "Jumlah maksimum antena" + wordMuteMax: "Jumlah maksimum karakter yang diperbolehkan dalam membisukan kata" + webhookMax: "Jumlah maksimum Webhook" + clipMax: "Jumlah maksimum Klip" + noteEachClipsMax: "Jumlah maksimum catatan di dalam Klip" + userListMax: "Jumlah maksimum daftar pengguna" + userEachUserListsMax: "Jumlah maksimum pengguna dalam dsftar pengguna" + rateLimitFactor: "Batas kecepatan" + descriptionOfRateLimitFactor: "Batas kecepatan yang rendah tidak begitu membatasi, batas kecepatan tinggi lebih membatasi. " + canHideAds: "Dapat menyembunyikan iklan" + _condition: + isLocal: "Pengguna lokal" + isRemote: "Pengguna remote" + createdLessThan: "Telah berlalu kurang dari X sejak pembuatan akun" + createdMoreThan: "Telah berlalu lebih dari X sejak pembuatan akun" + followersLessThanOrEq: "Memiliki pengikut X atau kurang dari tersebut" + followersMoreThanOrEq: "Memiliki pengikut X atau lebih dari tersebut" + followingLessThanOrEq: "Mengikuti X pengguna atau kurang dari itu" + followingMoreThanOrEq: "Mengikuti X pengguna atau lebih dari itu" + and: "Kondisi-AND" + or: "Kondisi-OR" + not: "Kondisi-NOT" +_sensitiveMediaDetection: + description: "Mengurangi usaha moderasi server dengan mengenali media NSFW srcara otomatis menggunakan Machine Learning. Fungsi ini akan sedikit menaikkan beban peladen." + sensitivity: "Sensitivitas deteksi" + sensitivityDescription: "Mengurangi sensitivitas akan mengurangi misdeteksi (false positive) sedangkan meningkatkannya akan menambah misdeteksi (false positive)." + setSensitiveFlagAutomatically: "Tandai sebagai NSFW" + setSensitiveFlagAutomaticallyDescription: "Hasil dari deteksi internal akan dipertahankan meskipun fungsi ini dimatikan." + analyzeVideos: "Nyalakan analisis terhadap video" + analyzeVideosDescription: "Analisa video sebagai tambahan dari gambar. Ini akan sedikit meningkatkan beban ke peladen." _emailUnavailable: used: "Alamat surel ini telah digunakan" format: "Format tidak valid." @@ -1112,6 +1287,24 @@ _plugin: install: "Memasang plugin" installWarn: "Mohon jangan memasang plugin yang tidak dapat dipercayai." manage: "Manajemen plugin" +_preferencesBackups: + list: "Cadangan yang dibuat" + saveNew: "Simpan cadangan baru" + loadFile: "Muat dari berkas" + apply: "Terapkan pada perangkat ini" + save: "Simpan perubahan" + inputName: "Mohon masukkan nama untuk cadangan ini" + cannotSave: "Gagal menyimpan" + nameAlreadyExists: "Cadangan bernama \"{name}\" sudah ada. Mohon gunakan nama lain." + applyConfirm: "Apakah kamu yakin untuk menerapkan cadangan \"{name}\" ke perangkat ini? Pengaturan yang sudah ada di perangkat ini nantinya akan ditimpa." + saveConfirm: "Simpan cadangan sebagai {name}?" + deleteConfirm: "Hapus cadangan {name}?" + renameConfirm: "Ganti cadangan ini dari \"{old}\" ke \"{new}\"?" + noBackups: "Tidak ada cadangan. Kamu dapat mencadangkan pengaturanmu di peladen ini dengan menggunakan \"Buat cadangan baru\"." + createdAt: "Dibuat pada: {date} {time}" + updatedAt: "Diperbarui pada: {date} {time}" + cannotLoad: "Gagal memuat" + invalidFile: "Format berkas tidak valid" _registry: scope: "Lingkup" key: "Kunci" @@ -1131,70 +1324,6 @@ _nsfw: respect: "Sembunyikan media NSFW" ignore: "Jangan sembunyikan media NSFW" force: "Sembunyikan semua media" -_mfm: - cheatSheet: "Contekan MFM" - intro: "MFM adalah Misskey-exclusive Markup Language yang dapat digunakan di banyak tempat. Berikut kamu bisa melihat daftar dari syntax MFM yang ada." - dummy: "Misskey membentangkan dunia Fediverse" - mention: "Sebut" - mentionDescription: "Kamu dapat menentukan pengguna tertentu dengan menggunakan simbol-At dan nama engguna mereka." - hashtag: "Tagar" - hashtagDescription: "Kamu dapat menentukan tagar dengan menggunakan angka dan teks." - url: "URL" - urlDescription: "URL dapat ditampilkan." - link: "Tautan" - linkDescription: "Bagian tertentu dari teks dapat ditampilka sebagai URL." - bold: "Tebal" - boldDescription: "Sorot tulisan dengan membuatnya tebal." - small: "Kecil" - smallDescription: "Tampilkan konten kecil dan tipis." - center: "Tengah" - centerDescription: "Tampilkan konten di tengah." - inlineCode: "Kode (Dalam baris)" - inlineCodeDescription: "Menampilkan sorotan sintaks dalam baris untuk kode(program-)." - blockCode: "Kode (Blok)" - blockCodeDescription: "Menampilkan sorotan sintaks untuk kode(program-) multi baris dalam sebuah blok." - inlineMath: "Matematika (Dalam baris)" - inlineMathDescription: "Menampilkan formula matematika (KaTeX) dalam baris." - blockMath: "Matematika (Blok)" - blockMathDescription: "Menampilkan formula matematika (KaTeX) multibaris dalam sebuah blok." - quote: "Kutip" - quoteDescription: "Menampilkan konten sebagai kutipan." - emoji: "Emoji kustom" - emojiDescription: "Emoji kustom dapat ditampilkan dengan mengurung nama emoji kustom menggunakan tanda titik dua." - search: "Penelusuran" - searchDescription: "Menampilkan kotak pencarian dengan teks yang sudah dimasukkan." - flip: "Balik" - flipDescription: "Balikkan konten secara horizontal atau vertikal." - jelly: "Animasi (Jelly)" - jellyDescription: "Menerapkan animasi seperti jelly" - tada: "Animasi (Tada)" - tadaDescription: "Menerapkan animasi seperti \"Kejutan!\"." - jump: "Animasi (Loncat)" - jumpDescription: "Menerapkan animasi melompat." - bounce: "Animasi (Melambung)" - bounceDescription: "Menerapkan animasi melambung." - shake: "Animasi (Goyang)" - shakeDescription: "Menerapkan animasi bergoyang." - twitch: "Animasi (Cubit)" - twitchDescription: "Terapkan animasi cubit yang kuat." - spin: "Animasi (Putar)" - spinDescription: "Terapkan animasi putar." - x2: "Besar" - x2Description: "Tampilkan konten menjadi besar." - x3: "Lebih besar" - x3Description: "Tampilkan konten menjadi lebih besar." - x4: "Sangat besar" - x4Description: "Tampilka konten menjadi sangat besar." - blur: "Buram" - blurDescription: "Konten dapat diburamkan dengan efek ini. Konten dapat ditampilkan dengan jelas dengan melayangkan kursor tetikus di atasnya." - font: "Font" - fontDescription: "Setel font yang ditampilkan untuk konten." - rainbow: "Pelangi" - rainbowDescription: "Membuat konten muncul dalam warna pelangi." - sparkle: "Kelap-kelip" - sparkleDescription: "Memberikan konten efek partikel kelap-kelip." - rotate: "Putar" - rotateDescription: "Putar konten sesuai sudut yang ditentukan." _instanceTicker: none: "Jangan tampilkan" remote: "Tampilkan untuk pengguna luar" @@ -1324,6 +1453,7 @@ _ago: weeksAgo: "{n} minggu lalu" monthsAgo: "{n} bulan lalu" yearsAgo: "{n} tahun lalu" + invalid: "Tidak ada sama sekali disini" _time: second: "detik" minute: "menit" @@ -1352,17 +1482,19 @@ _tutorial: step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar Misskey." step7_2: "Jika kamu ingin mempelajari lebih lanjut tentang Misskey, cobalah berkunjung ke bagian {help}." step7_3: "Semoga berhasil dan bersenang-senanglah! 🚀" + step8_1: "Yang terakhir, apakah kamu ingin menyalakam pemberitahuan push?" + step8_2: "Menyalakan ini akan memungkinkan kamu menerima pemberitahuan untuk sebutan, reaksi, ikuti, dll. Bahkan ketika Misskey sedang tidak dibuka." step8_3: "Kamu dapat mengganti pengaturan ini nanti." _2fa: alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." - registerDevice: "Daftarkan perangkat baru" - registerKey: "Daftarkan kunci keamanan baru" step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu." step2: "Lalu, pindai kode QR yang ada di layar." step2Url: "Di aplikasi desktop, masukkan URL berikut:" step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan." step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu." securityKeyInfo: "Kamu dapat memasang otentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau otentikasi PIN pada perangkatmu." + removeKeyConfirm: "Hapus cadangan {name}?" + renewTOTPCancel: "Tidak sekarang." _permissions: "read:account": "Lihat informasi akun" "write:account": "Sunting informasi akun" @@ -1397,18 +1529,20 @@ _permissions: "read:gallery-likes": "Lihat daftar postingan galeri yang disukai" "write:gallery-likes": "Sunting daftar postingan galeri yang disukai" _auth: + shareAccessTitle: "Mendapatkan ijin akses aplikasi" shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?" shareAccessAsk: "Apakah kamu ingin mengijinkan aplikasi ini untuk mengakses akun kamu?" + permission: "{name} meminta ijin berikut" permissionAsk: "Aplikasi ini membutuhkan beberapa ijin, yaitu:" pleaseGoBack: "Mohon kembali ke aplikasi kamu" callback: "Mengembalikan kamu ke aplikasi" denied: "Akses ditolak" + pleaseLogin: "Mohon masuk untuk otorisasi aplikasi." _antennaSources: all: "Semua catatan" homeTimeline: "Catatan dari pengguna yang diikuti" users: "Catatan dari pengguna tertentu" userList: "Catatan dari daftar tertentu" - userGroup: "Catatan dari pengguna dalam grup yang ditentukan" _weekday: sunday: "Minggu" monday: "Senin" @@ -1483,8 +1617,6 @@ _visibility: followersDescription: "Catat ke pengikut saja" specified: "Langsung" specifiedDescription: "Catat ke pengguna yang ditentukan saja" - localOnly: "Hanya lokal" - localOnlyDescription: "Hanya dapat dilihat di instansi lokal" _postForm: replyPlaceholder: "Balas ke catatan ini..." quotePlaceholder: "Kutip catatan ini..." @@ -1622,12 +1754,9 @@ _notification: youGotReply: "{name} membalas kamu" youGotQuote: "{name} mengutip kamu" youRenoted: "{name} me-renote kamu" - youGotMessagingMessageFromUser: "{name} mengirimi kamu pesan" - youGotMessagingMessageFromGroup: "Sebuah pesan telah dikirim ke grup {name}" youWereFollowed: "Mengikuti kamu" youReceivedFollowRequest: "Kamu menerima permintaan mengikuti" yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima" - youWereInvitedToGroup: "Telah diundang ke grup" pollEnded: "Hasil Kuesioner telah keluar" unreadAntennaNote: "Antena {name}" emptyPushNotificationMessage: "Pembaruan notifikasi dorong" @@ -1643,7 +1772,6 @@ _notification: pollEnded: "Jajak pendapat berakhir" receiveFollowRequest: "Permintaan mengikuti diterima" followRequestAccepted: "Permintaan mengikuti disetujui" - groupInvited: "Diundang ke grup" app: "Pemberitahuan dari aplikasi" _actions: followBack: "Ikuti Kembali" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 12bbc78d62..89d456d1fe 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -11,7 +11,7 @@ password: "Password" forgotPassword: "Hai dimenticato la password?" fetchingAsApObject: "Recuperando dal Fediverso..." ok: "OK" -gotIt: "Ho capito" +gotIt: "ok!" cancel: "Annulla" noThankYou: "No grazie" enterUsername: "Inserisci un nome utente" @@ -46,7 +46,7 @@ copyContent: "Copia il contenuto" copyLink: "Copia il link" delete: "Elimina" deleteAndEdit: "Elimina e modifica" -deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verrano eliminate anche tutte le reazioni, Rinote e risposte collegate." +deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate." addToList: "Aggiungi alla lista" sendMessage: "Invia messaggio" copyRSS: "Copia RSS" @@ -75,7 +75,7 @@ lists: "Liste" noLists: "Nessuna lista" note: "Nota" notes: "Note" -following: "Follows" +following: "Follow" followers: "Followers" followsYou: "Ti segue" createList: "Aggiungi una nuova lista" @@ -84,17 +84,17 @@ error: "Errore" somethingHappened: "Si è verificato un problema" retry: "Riprova" pageLoadError: "Caricamento pagina non riuscito. " -pageLoadErrorDescription: "Questo viene normalmente causato dalla rete o dalla cache del browser. Si prega di pulire la cache, o di attendere e riprovare più tardi." +pageLoadErrorDescription: "Questo problema viene normalmente causato da errori di rete o dalla cache del browser. Si prega di pulire la cache, o di attendere e riprovare più tardi." serverIsDead: "Il server non risponde. Si prega di attendere e riprovare più tardi." youShouldUpgradeClient: "Per visualizzare la pagina è necessario aggiornare il client alla nuova versione e ricaricare." enterListName: "Nome della lista" privacy: "Privacy" -makeFollowManuallyApprove: "Richiedi di approvare i follower manualmente" +makeFollowManuallyApprove: "Approva i follower manualmente" defaultNoteVisibility: "Privacy predefinita delle note" follow: "Segui" followRequest: "Richiesta di follow" followRequests: "Richieste di follow" -unfollow: "Smetti di seguire" +unfollow: "Non seguire" followRequestPending: "Richiesta in approvazione" enterEmoji: "Inserisci emoji" renote: "Rinota" @@ -103,6 +103,8 @@ renoted: "Rinotato!" cantRenote: "È impossibile rinotare questa nota." cantReRenote: "È impossibile rinotare una Rinota." quote: "Cita" +inChannelRenote: "Rinota nel canale" +inChannelQuote: "Cita nel canale" pinnedNote: "Nota fissata" pinned: "Fissa sul profilo" you: "Tu" @@ -119,7 +121,7 @@ markAsSensitive: "Segna come sensibile" unmarkAsSensitive: "Segna come non sensibile" enterFileName: "Nome del file" mute: "Silenzia" -unmute: "Riattiva" +unmute: "Riattiva l'audio" block: "Blocca" unblock: "Sblocca" suspend: "Sospendi" @@ -129,6 +131,7 @@ unblockConfirm: "Vuoi davvero sbloccare il profilo?" suspendConfirm: "Vuoi sospendere questo profilo?" unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?" selectList: "Seleziona una lista" +selectChannel: "Seleziona canale" selectAntenna: "Scegli un'antenna" selectWidget: "Seleziona il riquadro" editWidgets: "Modifica i riquadri" @@ -140,7 +143,7 @@ emojiName: "Nome dell'emoji" emojiUrl: "URL dell'emoji" addEmoji: "Aggiungi un emoji" settingGuide: "Configurazione suggerita" -cacheRemoteFiles: "Memorizzazione nella cache dei file remoti" +cacheRemoteFiles: "Memorizza i file remoti nella cache" cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime." flagAsBot: "Io sono un robot" flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene d’interazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot." @@ -151,14 +154,14 @@ flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le rispost autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui" addAccount: "Aggiungi profilo" loginFailed: "Accesso non riuscito" -showOnRemote: "Sfoglia sull'istanza remota" +showOnRemote: "Leggi sull'istanza remota" general: "Generali" wallpaper: "Sfondo" setWallpaper: "Imposta sfondo" removeWallpaper: "Elimina lo sfondo" searchWith: "Cerca: {q}" youHaveNoLists: "Non hai ancora creato nessuna lista" -followConfirm: "Sei sicur@ di voler seguire {name}?" +followConfirm: "Vuoi seguire {name}?" proxyAccount: "Profilo proxy" proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti." host: "Server remoto" @@ -206,19 +209,19 @@ intro: "L'installazione di Misskey è terminata! Si prega di creare il profilo a done: "Fine" processing: "In elaborazione" preview: "Anteprima" -default: "Predefinito" +default: "Medio" defaultValueIs: "Predefinito: {value}" noCustomEmojis: "Nessun emoji" noJobs: "Nessun lavoro" federating: "Federazione" blocked: "Bloccato" suspended: "Sospensione" -all: "Tutti" +all: "Tutte" subscribing: "Iscrizione" publishing: "Pubblicazione" notResponding: "Nessuna risposta" instanceFollowing: "Seguiti dall'istanza" -instanceFollowers: "Followers dell'istanza" +instanceFollowers: "Follower dell'istanza" instanceUsers: "Utenti dell'istanza" changePassword: "Aggiorna Password" security: "Sicurezza" @@ -231,7 +234,7 @@ more: "Di più!" featured: "Tendenze" usernameOrUserId: "Nome utente o ID utente" noSuchUser: "Nessun utente trovato" -lookup: "Cercare" +lookup: "Cerca" announcements: "Annunci" imageUrl: "URL dell'immagine" remove: "Elimina" @@ -256,6 +259,8 @@ noMoreHistory: "Non c'è più cronologia da visualizzare" startMessaging: "Nuovo messaggio" nUsersRead: "Letto da {n} persone" agreeTo: "Sono d'accordo con {0}" +agreeBelow: "Accetto quanto riportato sotto" +basicNotesBeforeCreateAccount: "Note importanti" tos: "Termini di servizio" start: "Inizia!" home: "Home" @@ -283,7 +288,7 @@ selectFolders: "Seleziona cartella" renameFile: "Rinomina file" folderName: "Nome della cartella" createFolder: "Nuova cartella" -renameFolder: "Rinominare cartella" +renameFolder: "Rinomina cartella" deleteFolder: "Elimina cartella" addFile: "Allega" emptyDrive: "Il Drive è vuoto" @@ -300,7 +305,7 @@ avatar: "Foto del profilo" banner: "Intestazione" nsfw: "Contenuti sensibili" whenServerDisconnected: "Quando la connessione col server è persa" -disconnectedFromServer: "Disconness@ dal server" +disconnectedFromServer: "Il server si è disconnesso" reload: "Ricarica" doNothing: "Nessun'azione" reloadConfirm: "Vuoi ricaricare?" @@ -311,9 +316,9 @@ reject: "Rifiuta" normal: "Normale" instanceName: "Nome dell'istanza" instanceDescription: "Descrizione dell'istanza" -maintainerName: "Nome dell'Amministratore" -maintainerEmail: "Indirizzo e-mail dell'Amministratore" -tosUrl: "Termini di servizio URL" +maintainerName: "Nome dell'amministratore" +maintainerEmail: "Indirizzo e-mail dell'amministratore" +tosUrl: "URL dei termini del servizio e della privacy" thisYear: "Anno" thisMonth: "Mese" today: "Oggi" @@ -322,13 +327,13 @@ monthX: "{month}" yearX: "{year}" pages: "Pagine" integration: "App collegate" -connectService: "Connessione" -disconnectService: "Disconnessione " -enableLocalTimeline: "Abilita Timeline locale" -enableGlobalTimeline: "Abilita Timeline federata" +connectService: "Connetti" +disconnectService: "Disconnetti" +enableLocalTimeline: "Abilita la timeline locale" +enableGlobalTimeline: "Abilita la timeline federata" disablingTimelinesInfo: "Anche disabilitandole, gli Amministratori e i Moderatori potranno comunque accedervi." registration: "Iscriviti" -enableRegistration: "Permettere nuove registrazioni" +enableRegistration: "Consenti a chiunque di registrarsi" invite: "Invita" driveCapacityPerLocalAccount: "Capienza del Drive per profilo locale" driveCapacityPerRemoteAccount: "Capienza del Drive per profilo remoto" @@ -388,16 +393,19 @@ about: "Informazioni" aboutMisskey: "Informazioni di Misskey" administrator: "Amministratore" token: "Token" -twoStepAuthentication: "Autenticazione a due fattori" +2fa: "Autenticazione a due fattori" +totp: "App di autenticazione" +totpDescription: "Inserisci un codice OTP tramite un'app di autenticazione" moderator: "Moderatore" moderation: "moderazione" nUsersMentioned: "{n} profili menzionati" +securityKeyAndPasskey: "Chiave di sicurezza e accesso" securityKey: "Chiave di sicurezza" -securityKeyName: "Nome della chiave" -registerSecurityKey: "Registra una chiave di sicurezza" lastUsed: "Ultima attività" +lastUsedAt: "Uso più recente: {t}" unregister: "Annulla l'iscrizione" passwordLessLogin: "Accedi senza password" +passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza" resetPassword: "Reimposta password" newPasswordIs: "La tua nuova password è「{password}」" reduceUiAnimation: "Ridurre le animazioni dell'interfaccia" @@ -412,24 +420,15 @@ markAsReadAllTalkMessages: "Segna tutte le chat come lette" help: "Guida" inputMessageHere: "Scrivi messaggio qui" close: "Chiudi" -group: "Gruppo" -groups: "Gruppi" -createGroup: "Nuovo gruppo" -ownedGroups: "I miei gruppi" -joinedGroups: "Gruppi a cui mi sono unit@" invites: "Inviti" -groupName: "Nome del gruppo" members: "Membri" transfer: "Trasferisci" -messagingWithUser: "Iniziare una chat con un altr@ utente" -messagingWithGroup: "Chattare in gruppo" title: "Titolo" text: "Testo" enable: "Abilita" next: "Avanti" retype: "Conferma" noteOf: "Note di {user}" -inviteToGroup: "Invitare al gruppo" quoteAttached: "Citazione allegata" quoteQuestion: "Vuoi aggiungere una citazione?" noMessagesYet: "Ancora nessuna chat" @@ -451,20 +450,18 @@ passwordMatched: "Corretta" passwordNotMatched: "Le password non corrispondono." signinWith: "Accedi con {x}" signinFailed: "Autenticazione non riuscita. Controlla la tua password e nome utente." -tapSecurityKey: "Premi la chiave di sicurezza" or: "oppure" language: "Lingua" uiLanguage: "Lingua di visualizzazione dell'interfaccia" -groupInvited: "Invitat@ al gruppo" aboutX: "Informazioni su {x}" emojiStyle: "Stile emoji" native: "Nativo" disableDrawer: "Non mostrare il menù sul drawer" -youHaveNoGroups: "Nessun gruppo" -joinOrCreateGroup: "Puoi creare il tuo gruppo o essere invitat@ a gruppi che già esistono." +showNoteActionsOnlyHover: "Mostra le azioni delle Note solo al passaggio del mouse" noHistory: "Nessuna cronologia" signinHistory: "Storico degli accessi al profilo" -disableAnimatedMfm: "Disabilità i MFM animati" +enableAdvancedMfm: "Attiva MFM avanzati" +enableAnimatedMfm: "Attiva MFM animati" doing: "In corso..." category: "Categoria" tags: "Tag" @@ -474,7 +471,7 @@ existingAccount: "Profilo esistente" regenerate: "Generare di nuovo" fontSize: "Dimensione carattere" noFollowRequests: "Non hai alcuna richiesta di follow" -openImageInNewTab: "Aprire immagini in una nuova scheda" +openImageInNewTab: "Apri le immagini in un nuovo tab" dashboard: "Pannello di controllo" local: "Locale" remote: "Remoto" @@ -603,7 +600,7 @@ smtpPass: "Password" emptyToDisableSmtpAuth: "Lasciare il nome utente e la password vuoti per disabilitare la verifica SMTP" smtpSecure: "Usare la porta SSL/TLS implicito per le connessioni SMTP" smtpSecureInfo: "Disabilitare quando è attivo STARTTLS." -testEmail: "Testare la consegna di posta elettronica" +testEmail: "Testa la consegna di posta elettronica" wordMute: "Filtri parole" regexpError: "errore regex" regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:" @@ -616,7 +613,7 @@ metrics: "Statistiche" overview: "Anteprima" logs: "Log" delayed: "Ritardo" -database: "Base di dati" +database: "Base dati" channel: "Canale" create: "Crea" notificationSetting: "Impostazioni notifiche" @@ -705,7 +702,7 @@ narrow: "Stretto" reloadToApplySetting: "Le tue preferenze verranno impostate dopo il ricaricamento della pagina. Vuoi ricaricare adesso?" needReloadToApply: "È necessario riavviare per rendere effettive le modifiche." showTitlebar: "Visualizza la barra del titolo" -clearCache: "Svuota cache" +clearCache: "Svuota la cache" onlineUsersCount: "{n} utenti online" nUsers: "{n} utenti" nNotes: "{n}Note" @@ -730,9 +727,9 @@ currentVersion: "Versione attuale" latestVersion: "Ultima versione" youAreRunningUpToDateClient: "Stai usando la versione più recente del client." newVersionOfClientAvailable: "Una nuova versione del tuo client è disponibile." -usageAmount: "In utilizzo" +usageAmount: "In uso" capacity: "Capacità" -inUse: "In utilizzo" +inUse: "In uso" editCode: "Modifica codice" apply: "Applica" receiveAnnouncementFromInstance: "Ricevi i messaggi informativi dall'istanza" @@ -783,6 +780,7 @@ popularPosts: "Le più visualizzate" shareWithNote: "Condividere in nota" ads: "Pubblicità" expiration: "Scadenza" +startingperiod: "Periodo di inizio" memo: "Promemoria" priority: "Priorità" high: "Alta" @@ -814,11 +812,12 @@ pubSub: "Publish/Subscribe del profilo" lastCommunication: "La comunicazione più recente" resolved: "Risolto" unresolved: "Non risolto" -breakFollow: "Smetti di seguire" +breakFollow: "Non seguire" +breakFollowConfirm: "Vuoi davvero togliere follower?" itsOn: "Abilitato" itsOff: "Disabilitato" emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi" -unread: "Non letto" +unread: "Non lette" filter: "Filtri" controlPanel: "Pannello di controllo" manageAccounts: "Gestisci i profili" @@ -834,8 +833,6 @@ deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" incorrectPassword: "La password è errata." voteConfirm: "Votare per「{choice}」?" hide: "Nascondere" -leaveGroup: "Esci dal gruppo" -leaveGroupConfirm: "Uscire da「{name}」?" useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile" welcomeBackWithName: "Ciao, {name}! Eccoti di nuovo!" clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email." @@ -851,16 +848,20 @@ instanceDefaultLightTheme: "Istanza, tema luminoso predefinito." instanceDefaultDarkTheme: "Istanza, tema scuro predefinito." instanceDefaultThemeDescription: "Compilare il codice del tema nel modulo dell'oggetto." mutePeriod: "Durata del mute" +period: "Scadenza" indefinitely: "Non scade" tenMinutes: "10 minuti" oneHour: "1 ora" oneDay: "1 giorno" oneWeek: "1 settimana" +oneMonth: "Un mese" reflectMayTakeTime: "Potrebbe essere necessario un po' di tempo perché ciò abbia effetto." failedToFetchAccountInformation: "Impossibile recuperare le informazioni sul profilo" -rateLimitExceeded: "Superato il limite di velocità." -cropImage: "Ritaglio dell'immagine" -cropImageAsk: "Si desidera ritagliare l'immagine?" +rateLimitExceeded: "Superato il limite di richieste." +cropImage: "Ritaglia l'immagine" +cropImageAsk: "Vuoi ritagliare l'immagine?" +cropYes: "Ritaglia" +cropNo: "Non ritagliare" file: "Allegati" recentNHours: "Ultime {n} ore" recentNDays: "Ultimi {n} giorni" @@ -939,6 +940,21 @@ cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché preset: "Preimpostato" selectFromPresets: "Seleziona preimpostato" achievements: "Obiettivi raggiunti" +gotInvalidResponseError: "Risposta del server non valida" +gotInvalidResponseErrorDescription: "Il server potrebbe essere irraggiungibile o in manutenzione. Riprova più tardi." +thisPostMayBeAnnoying: "Questa nota potrebbe essere offensiva" +thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale" +thisPostMayBeAnnoyingCancel: "Annulla" +thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso" +collapseRenotes: "Comprimi i Rinota già letti" +internalServerError: "Errore interno del server" +internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server" +copyErrorInfo: "Copia le informazioni sull'errore" +joinThisServer: "Registrati su questa istanza" +exploreOtherServers: "Trova altre istanze" +letsLookAtTimeline: "Sbircia la timeline" +disableFederationWarn: "Disabilita la federazione. Questo cambiamento non rende le pubblicazioni private. Di solito non è necessario abilitare questa opzione." +invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi inserire un codice d'invito valido. Puoi richiedere un codice all'amministratore." _achievements: earnedAt: "Data di conseguimento" _types: @@ -994,53 +1010,53 @@ _achievements: flavor: "Hai molto da scrivere?" _login3: title: "Principiante I" - description: "Accedi per un totale di 3 giorni" + description: "Hai totalizzato 3 accessi!" flavor: "Da oggi, chiamatemi Misskist" _login7: title: "Principiante II" - description: "Accedi per un totale di 7 giorni" + description: "Hai totalizzato 7 accessi!" flavor: "Ti sembra di avere la situazione sotto controllo?" _login15: title: "Principiante III" - description: "Accedi per un totale di 15 giorni" + description: "Hai totalizzato 15 accessi!" _login30: title: "Misskist I" - description: "Accedi per un totale di 30 giorni" + description: "Hai totalizzato 30 accessi!" _login60: title: "Misskeist II" - description: "Accedi per un totale di 60 giorni" + description: "Hai totalizzato 60 accessi!" _login100: title: "Misskeist III" - description: "Accedi per un totale di 100 giorni" + description: "Hai totalizzato 100 accessi!" flavor: "Violent Misskeist" _login200: - title: "Regolare I" - description: "Accedi per un totale di 200 giorni" + title: "Regolare I livello" + description: "Hai totalizzato 200 accessi!" _login300: - title: "Regolare II" - description: "Accedi per un totale di 300 giorni" + title: "Regolare II livello" + description: "Hai totalizzato 300 accessi!" _login400: - title: "Regolare III" - description: "Accedi per un totale di 400 giorni" + title: "Regolare III livello" + description: "Hai totalizzato 400 accessi!" _login500: - title: "Professionista I" - description: "Accedi per un totale di 500 giorni" + title: "Professionista I livello" + description: "Hai totalizzato 500 accessi!" flavor: "Amici cari, mi piacciono le Note" _login600: - title: "Professionista II" - description: "Accedi per un totale di 600 giorni" + title: "Professionista II livello" + description: "Hai totalizzato 600 accessi!" _login700: - title: "Professionista III" - description: "Accedi per un totale di 700 giorni" + title: "Professionista III livello" + description: "Hai totalizzato 700 accessi!" _login800: - title: "Maestro di Note I" - description: "Accedi per un totale di 800 giorni" + title: "Maestro di Note I livello" + description: "Hai totalizzato 800 accessi!" _login900: - title: "Maestro di Note II" - description: "Accedi per un totale di 900 giorni" + title: "Maestro di Note II livello" + description: "Hai totalizzato 900 accessi!" _login1000: - title: "Maestro di Note III" - description: "Accedi per un totale di 1.000 giorni" + title: "Maestro di Note III livello" + description: "Hai totalizzato 1000 accessi!" flavor: "Grazie per aver usato Misskey!" _noteClipped1: title: "Devo clippare!" @@ -1052,7 +1068,7 @@ _achievements: title: "Fornitura stelline" description: "Qualcuno ha preferito una delle tue Note" _profileFilled: - title: "Perfettamente" + title: "Preparazione perfetta!" description: "Imposta il tuo profilo" _markedAsCat: title: "Io sono un gatto" @@ -1075,7 +1091,7 @@ _achievements: description: "Hai seguito 300 profili" _followers1: title: "Il primo profilo tuo Follower" - description: "Hai ottenuto il tuo primo Follower" + description: "Hai ottenuto il tuo primo profilo Follower" _followers10: title: "Follow me!" description: "Hai ottenuto 10 profili Follower" @@ -1323,72 +1339,6 @@ _nsfw: respect: "Nascondere i media segnati come sensibli" ignore: "Visualizzare i media segnati come sensibili" force: "Nascondere tutti i media" -_mfm: - cheatSheet: "Bigliettino MFM" - intro: "MFM è un linguaggio Markdown particolare che si può usare in diverse parti di Misskey. Qui puoi visualizzare a colpo d'occhio tutta la sintassi MFM utile." - dummy: "Il Fediverso si espande con Misskey" - mention: "Menzioni" - mentionDescription: "Si può menzionare un utente specifico digitando il suo nome utente subito dopo il segno @." - hashtag: "Hashtag" - hashtagDescription: "Per indicare un hashtag si può usare il segno numerico + tag." - url: "URL" - urlDescription: "È possibile indicare gli URL" - link: "Link" - linkDescription: "È possibile associare specifici intervalli di testo agli URL" - bold: "Grassetto" - boldDescription: "Il testo può essere grassettato per enfasi" - small: "vistosamente" - smallDescription: "Il contenuto può essere visualizzato più piccolo e più sottile" - center: "centratura" - centerDescription: "Il contenuto può essere centrato" - inlineCode: "Codice (inline)" - inlineCodeDescription: "Evidenziazione della sintassi in linea di programmi e altro codice" - blockCode: "Codice (blocco)" - blockCodeDescription: "Evidenziazione della sintassi di programmi multilinea e di altro codice in blocchi" - inlineMath: "Espressione matematica(Immersione)" - inlineMathDescription: "Visualizza le formule (KaTeX) in linea." - blockMath: "Formula matematica (blocco)" - blockMathDescription: "Visualizzazione di formule multilinea (KaTeX) in blocchi." - quote: "Cita il nota" - quoteDescription: "Può indicare che il contenuto è una citazione." - emoji: "Emoji personalizzati" - emojiDescription: "Utilizzare i due punti per racchiudere il nome di un'emoji personalizzata e visualizzarla." - search: "Cerca" - searchDescription: "È possibile visualizzare una casella di ricerca precompilata." - flip: "Inverti" - flipDescription: "Capovolgere il contenuto verso l'alto o verso il basso, a sinistra o a destra." - jelly: "Animazione (Biyon Biyon)." - jellyDescription: "Dà un'animazione di salto." - tada: "Animazione (jang)." - tadaDescription: "Ta-da! dà un'animazione che assomiglia a." - jump: "Animazione(salto)" - jumpDescription: "Da un animazione che salta su e giù." - bounce: "Animazione(rimbalzo)" - bounceDescription: "Rende il testo rimbalzante" - shake: "rimbalzante" - shakeDescription: "Rende il testo traballante" - twitch: "testo" - twitchDescription: "Fa tremare il testo" - spin: "Animazione (rotazione)" - spinDescription: "Fornisce un'animazione rotante." - x2: "Più grande" - x2Description: "Mostra il contenuto ingrandito." - x3: "Molto più grande" - x3Description: "Mostra il contenuto molto più ingrandito." - x4: "Estremamente più grande" - x4Description: "Mostra il contenuto estremamente più ingrandito." - blur: "Sfocatura" - blurDescription: "È possibile rendere sfocato il contenuto. Spostando il cursore su di esso tornerà visibile chiaramente." - font: "Tipo di carattere" - fontDescription: "Puoi scegliere il tipo di carattere per il contenuto." - rainbow: "Arcobaleno" - rainbowDescription: "Arcobaleno il contenuto." - sparkle: "brillantini" - sparkleDescription: "Aggiungere effetti particellari scintillanti." - rotate: "Ruota" - rotateDescription: "Ruota con un angolo specificato." - plain: "Testo semplice" - plainDescription: "Disattiva tutta la sintassi interna." _instanceTicker: none: "Nascondi" remote: "Mostra solo per i profili remoti" @@ -1518,6 +1468,7 @@ _ago: weeksAgo: "{n} sett. fa" monthsAgo: "{n} mesi fa" yearsAgo: "{n} anni fa" + invalid: "Niente da visualizzare" _time: second: "s" minute: "min" @@ -1526,10 +1477,10 @@ _time: _tutorial: title: "Come usare Misskey" step1_1: "Eccoci!" - step1_2: "Questa pagina si chiama una \" Timeline \". Mostra in ordine cronologico le \" note \" delle persone che segui." - step1_3: "Attualmente la tua Timeline è vuota perché non segui alcun profilo e non hai pubblicato alcuna nota ancora." + step1_2: "Questa pagina si chiama \"Timeline \" e mostra in ordine cronologico le \"note\" delle persone che segui." + step1_3: "Attualmente la tua Timeline è vuota perché non segui alcun profilo e non hai ancora pubblicato alcuna nota." step2_1: "Prima di scrivere una «Nota» o di seguire altri profili, prepara il tuo profilo!" - step2_2: "Aggiungere qualche informazione su di te aumenterà le tue possibilità di essere seguit@ da altre persone. " + step2_2: "Se aggiungi informazioni personali aumenterai le tue possibilità di essere seguit@ da altre persone. " step3_1: "Hai finito di impostare il tuo profilo?" step3_2: "Ora puoi pubblicare una «Nota». Proviamo subito! Premi il bottone con l'icona «penna» per iniziare a scrivere in una finestra di dialogo. " step3_3: "Scritto il testo della nota, puoi pubblicarla premendo il pulsante nella parte superiore destra della finestra di dialogo." @@ -1548,28 +1499,43 @@ _tutorial: step7_3: "Da ultimo, buon divertimento su Misskey! 🚀" step8_1: "Per concludere, vuoi attivare le notifiche push?" step8_2: "Attivandole, otterrai notifiche di follow, reazioni e menzioni anche quando Misskey è chiuso." - step8_3: "Puoi anche modificare questa impostazione successivamente." + step8_3: "Potrai modificare questa impostazione." _2fa: alreadyRegistered: "La configurazione è stata già completata." - registerDevice: "Aggiungi dispositivo" - registerKey: "Chiave di registro." + registerTOTP: "Registra un'app di autenticazione" + passwordToTOTP: "Inserire la password" step1: "Innanzitutto, installare sul dispositivo un'applicazione di autenticazione come {a} o {b}." step2: "Quindi, scansionare il codice QR visualizzato con l'app." + step2Click: "Cliccando sul codice QR, puoi registrarlo con l'app di autenticazione o il portachiavi installato sul tuo dispositivo." step2Url: "Nell'applicazione desktop inserire il seguente URL: " + step3Title: "Inserisci il codice di verifica" step3: "Inserite il token visualizzato nell'app e il gioco è fatto." step4: "D'ora in poi, quando si accede, si inserisce il token nello stesso modo." + securityKeyNotSupported: "Il tuo browser non supporta le chiavi di sicurezza." + registerTOTPBeforeKey: "Ti occorre un'app di autenticazione con OTP, prima di registrare la chiave di sicurezza." securityKeyInfo: "È possibile impostare il dispositivo per accedere utilizzando una chiave di sicurezza hardware che supporta FIDO2 o un'impronta digitale o un PIN sul dispositivo." + chromePasskeyNotSupported: "Le passkey di Chrome non sono attualmente supportate." + registerSecurityKey: "Registra la chiave di sicurezza" + securityKeyName: "Inserisci il nome della chiave" + tapSecurityKey: "Segui le istruzioni del browser e registra la chiave di sicurezza." + removeKey: "Elimina la chiave di sicurezza" + removeKeyConfirm: "Vuoi davvero eliminare \"{name}\"?" + whyTOTPOnlyRenew: "Se c'è una chiave di sicurezza attiva, non è possibile rimuovere l'app di autenticazione." + renewTOTP: "Riconfigura l'app di autenticazione" + renewTOTPConfirm: "I codici di verifica nelle app di autenticazione esistenti smetteranno di funzionare" + renewTOTPOk: "Ripristina" + renewTOTPCancel: "No grazie" _permissions: - "read:account": "Visualizzare le informazioni sul profilo" - "write:account": "Modificare le informazioni sul profilo" + "read:account": "Visualizza le informazioni sul profilo" + "write:account": "Modifica le informazioni sul profilo" "read:blocks": "Visualizza i profili bloccati" "write:blocks": "Gestisci i profili bloccati" - "read:drive": "Aprire il Drive" - "write:drive": "Gestire il Drive" + "read:drive": "Apri il Drive" + "write:drive": "Gestisci il Drive" "read:favorites": "Visualizza i tuoi preferiti" "write:favorites": "Gestisci i tuoi preferiti" "read:following": "Vedi le informazioni di follow" - "write:following": "Seguiti/ Smetti di seguire" + "write:following": "Seguire / Non seguire altri profili" "read:messaging": "Visualizzare la chat" "write:messaging": "Gestire la chat" "read:mutes": "Vedi i profili silenziati" @@ -1593,18 +1559,20 @@ _permissions: "read:gallery-likes": "Visualizza i contenuti della galleria." "write:gallery-likes": "Manipolazione dei \"Mi piace\" della galleria." _auth: + shareAccessTitle: "Permessi dell'applicazione" shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?" shareAccessAsk: "Vuoi autorizzare questa App ad accedere al tuo profilo?" + permission: "{name} richiede i permessi seguenti" permissionAsk: "Questa app richiede le seguenti autorizzazioni:" pleaseGoBack: "Si prega di ritornare sulla app" callback: "Ritornando sulla app" denied: "Accesso negato" + pleaseLogin: "Per favore accedi al tuo account per cambiare i permessi dell'applicazione" _antennaSources: all: "Tutte le note" homeTimeline: "Note dagli utenti che segui" users: "Note dagli utenti selezionati" userList: "Note dagli utenti della lista selezionata" - userGroup: "Note dagli utenti del gruppo selezionato" _weekday: sunday: "Domenica" monday: "Lunedì" @@ -1675,23 +1643,23 @@ _visibility: publicDescription: "Visibile per tutti sul Fediverso" home: "Home" homeDescription: "Visibile solo sulla timeline \"Home\"" - followers: "Followers" - followersDescription: "Visibile solo per i tuoi followers" - specified: "Diretta" + followers: "Follower" + followersDescription: "Visibile solo per i tuoi follower" + specified: "Nota diretta" specifiedDescription: "Visibile solo ai profili menzionati" - localOnly: "Soltanto locale" - localOnlyDescription: "Non visibile ai profili remoti" + disableFederation: "Interrompi la federazione" + disableFederationDescription: "Non spedire attività alle altre istanze remote" _postForm: - replyPlaceholder: "Nota la tua risposta.." - quotePlaceholder: "Cita Nota..." - channelPlaceholder: "Pubblica in canale" + replyPlaceholder: "Rispondi a questa nota..." + quotePlaceholder: "Cita questa nota..." + channelPlaceholder: "Pubblica sul canale..." _placeholders: - a: "Che succede?" - b: "È successo qualcosa?" - c: "Che cos'hai in mente?" + a: "Come va?" + b: "Hai qualcosa da raccontare? Inizia pure..." + c: "Stai pensando a qualcosa?" d: "Vuoi dire qualcosa?" - e: "Scrivi qualcosa qui" - f: "Aspettando che scriva..." + e: "Puoi scrivere qui..." + f: "Inizia pure a scrivere..." _profile: name: "Nome" username: "Nome utente" @@ -1707,7 +1675,7 @@ _profile: _exportOrImport: allNotes: "Tutte le note" favoritedNotes: "Note preferite" - followingList: "Follows" + followingList: "Follow" muteList: "Elenco profili silenziati" blockingList: "Elenco profili bloccati" userLists: "Liste" @@ -1722,9 +1690,9 @@ _charts: notesIncDec: "Variazione del numero di note" localNotesIncDec: "Variazione del numero di note locali" remoteNotesIncDec: "Variazione del numero di note distanti" - notesTotal: "Conteggio totale di note" + notesTotal: "Numero di note in totale" filesIncDec: "Variazione del numero dei file" - filesTotal: "Numero totale di file" + filesTotal: "Numero di file in totale" storageUsageIncDec: "Variazione dell'utilizzo dell'immagazzinamento" storageUsageTotal: "Utilizzo totale dell'immagazzinamento" _instanceCharts: @@ -1816,14 +1784,11 @@ _notification: fileUploaded: "File caricato correttamente" youGotMention: "{name} ti ha menzionato" youGotReply: "{name} ti ha risposto" - youGotQuote: "{name} ha citato il tuo Nota e ha detto" + youGotQuote: "{name} ha citato la tua Nota e ha detto" youRenoted: "{name} ha rinotato" - youGotMessagingMessageFromUser: "{name} ti ha mandato un messaggio" - youGotMessagingMessageFromGroup: "{name} ti ha mandato un messaggio nella chat" youWereFollowed: "Ha iniziato a seguirti" youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" - youWereInvitedToGroup: "Invitat@ al gruppo" pollEnded: "Risultati del sondaggio." unreadAntennaNote: "Antenna {name}" emptyPushNotificationMessage: "Le notifiche push sono state aggiornate." @@ -1839,7 +1804,7 @@ _notification: pollEnded: "Sondaggio chiuso." receiveFollowRequest: "Richiesta di follow ricevuta" followRequestAccepted: "Richiesta di follow accettata" - groupInvited: "Invito a un gruppo" + achievementEarned: "Risultato raggiunto" app: "Notifiche da applicazioni" _actions: followBack: "Segui" @@ -1872,3 +1837,6 @@ _deck: channel: "Canale" mentions: "Menzioni" direct: "Diretta" +_dialog: + charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})" + charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 09069e7801..8fee2726e8 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2,7 +2,7 @@ _lang_: "日本語" headlineMisskey: "ノートでつながるネットワーク" introMisskey: "ようこそ!Misskeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀" -poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォームMisskeyを使ったサービス(Misskeyインスタンスと呼ばれます)のひとつです。" +poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォームMisskeyのサーバーのひとつです。" monthAndDay: "{month}月 {day}日" search: "検索" notifications: "通知" @@ -18,7 +18,7 @@ enterUsername: "ユーザー名を入力" renotedBy: "{user}がRenote" noNotes: "ノートはありません" noNotifications: "通知はありません" -instance: "インスタンス" +instance: "サーバー" settings: "設定" basicSettings: "基本設定" otherSettings: "その他の設定" @@ -103,6 +103,8 @@ renoted: "Renoteしました。" cantRenote: "この投稿はRenoteできません。" cantReRenote: "RenoteをRenoteすることはできません。" quote: "引用" +inChannelRenote: "チャンネル内Renote" +inChannelQuote: "チャンネル内引用" pinnedNote: "ピン留めされたノート" pinned: "ピン留め" you: "あなた" @@ -161,13 +163,13 @@ searchWith: "検索: {q}" youHaveNoLists: "リストがありません" followConfirm: "{name}をフォローしますか?" proxyAccount: "プロキシアカウント" -proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがインスタンスに配達されないため、代わりにプロキシアカウントがフォローするようにします。" +proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" host: "ホスト" selectUser: "ユーザーを選択" recipient: "宛先" annotation: "注釈" federation: "連合" -instances: "インスタンス" +instances: "サーバー" registeredAt: "初観測" latestRequestReceivedAt: "直近のリクエスト受信" latestStatus: "直近のステータス" @@ -176,7 +178,7 @@ charts: "チャート" perHour: "1時間ごと" perDay: "1日ごと" stopActivityDelivery: "アクティビティの配送を停止" -blockThisInstance: "このインスタンスをブロック" +blockThisInstance: "このサーバーをブロック" operations: "操作" software: "ソフトウェア" version: "バージョン" @@ -187,15 +189,15 @@ jobQueue: "ジョブキュー" cpuAndMemory: "CPUとメモリ" network: "ネットワーク" disk: "ディスク" -instanceInfo: "インスタンス情報" +instanceInfo: "サーバー情報" statistics: "統計" clearQueue: "キューをクリア" clearQueueConfirmTitle: "キューをクリアしますか?" clearQueueConfirmText: "未配達の投稿は配送されなくなります。通常この操作を行う必要はありません。" clearCachedFiles: "キャッシュをクリア" clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?" -blockedInstances: "ブロックしたインスタンス" -blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。サブドメインもブロックされます。" +blockedInstances: "ブロックしたサーバー" +blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このサーバーとやり取りできなくなります。サブドメインもブロックされます。" muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" @@ -218,9 +220,9 @@ all: "全て" subscribing: "購読中" publishing: "配信中" notResponding: "応答なし" -instanceFollowing: "インスタンスのフォロー" -instanceFollowers: "インスタンスのフォロワー" -instanceUsers: "インスタンスのユーザー" +instanceFollowing: "サーバーのフォロー" +instanceFollowers: "サーバーのフォロワー" +instanceUsers: "サーバーのユーザー" changePassword: "パスワードを変更" security: "セキュリティ" retypedNotMatch: "入力が一致しません。" @@ -257,6 +259,8 @@ noMoreHistory: "これより過去の履歴はありません" startMessaging: "チャットを開始" nUsersRead: "{n}人が読みました" agreeTo: "{0}に同意" +agreeBelow: "下記に同意する" +basicNotesBeforeCreateAccount: "基本的な注意事項" tos: "利用規約" start: "始める" home: "ホーム" @@ -310,8 +314,8 @@ unwatch: "ウォッチ解除" accept: "許可" reject: "拒否" normal: "正常" -instanceName: "インスタンス名" -instanceDescription: "インスタンスの紹介" +instanceName: "サーバー名" +instanceDescription: "サーバーの紹介" maintainerName: "管理者の名前" maintainerEmail: "管理者のメールアドレス" tosUrl: "利用規約URL" @@ -341,7 +345,7 @@ basicInfo: "基本情報" pinnedUsers: "ピン留めユーザー" pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。" pinnedPages: "ピン留めページ" -pinnedPagesDescription: "インスタンスのトップページにピン留めしたいページのパスを改行で区切って記述します。" +pinnedPagesDescription: "サーバーのトップページにピン留めしたいページのパスを改行で区切って記述します。" pinnedClipId: "ピン留めするクリップのID" pinnedNotes: "ピン留めされたノート" hcaptcha: "hCaptcha" @@ -388,17 +392,20 @@ userList: "リスト" about: "情報" aboutMisskey: "Misskeyについて" administrator: "管理者" -token: "トークン" -twoStepAuthentication: "二段階認証" +token: "確認コード" +2fa: "二要素認証" +totp: "認証アプリ" +totpDescription: "認証アプリを使ってワンタイムパスワードを入力" moderator: "モデレーター" moderation: "モデレーション" nUsersMentioned: "{n}人が投稿" +securityKeyAndPasskey: "セキュリティキー・パスキー" securityKey: "セキュリティキー" -securityKeyName: "キーの名前" -registerSecurityKey: "セキュリティキーを登録する" lastUsed: "最後の使用" +lastUsedAt: "最後の使用: {t}" unregister: "登録を解除" -passwordLessLogin: "パスワード無しログイン" +passwordLessLogin: "パスワードレスログイン" +passwordLessLoginDescription: "パスワードを使用せず、セキュリティキーやパスキーなどのみでログインします" resetPassword: "パスワードをリセット" newPasswordIs: "新しいパスワードは「{password}」です" reduceUiAnimation: "UIのアニメーションを減らす" @@ -413,24 +420,15 @@ markAsReadAllTalkMessages: "すべてのチャットを既読にする" help: "ヘルプ" inputMessageHere: "ここにメッセージを入力" close: "閉じる" -group: "グループ" -groups: "グループ" -createGroup: "グループを作成" -ownedGroups: "所有グループ" -joinedGroups: "参加しているグループ" invites: "招待" -groupName: "グループ名" members: "メンバー" transfer: "譲渡" -messagingWithUser: "ユーザーとチャット" -messagingWithGroup: "グループでチャット" title: "タイトル" text: "テキスト" enable: "有効にする" next: "次" retype: "再入力" noteOf: "{user}のノート" -inviteToGroup: "グループに招待" quoteAttached: "引用付き" quoteQuestion: "引用として添付しますか?" noMessagesYet: "まだチャットはありません" @@ -452,20 +450,18 @@ passwordMatched: "一致しました" passwordNotMatched: "一致していません" signinWith: "{x}でログイン" signinFailed: "ログインできませんでした。ユーザー名とパスワードを確認してください。" -tapSecurityKey: "セキュリティキーにタッチ" or: "もしくは" language: "言語" uiLanguage: "UIの表示言語" -groupInvited: "グループに招待されました" aboutX: "{x}について" emojiStyle: "絵文字のスタイル" native: "ネイティブ" disableDrawer: "メニューをドロワーで表示しない" -youHaveNoGroups: "グループがありません" -joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。" +showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する" noHistory: "履歴はありません" signinHistory: "ログイン履歴" -disableAnimatedMfm: "動きのあるMFMを無効にする" +enableAdvancedMfm: "高度なMFMを有効にする" +enableAnimatedMfm: "動きのあるMFMを有効にする" doing: "やっています" category: "カテゴリ" tags: "タグ" @@ -542,7 +538,7 @@ updateRemoteUser: "リモートユーザー情報の更新" deleteAllFiles: "すべてのファイルを削除" deleteAllFilesConfirm: "すべてのファイルを削除しますか?" removeAllFollowing: "フォローを全解除" -removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。" +removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのサーバーがもう存在しなくなった場合などに実行してください。" userSuspended: "このユーザーは凍結されています。" userSilenced: "このユーザーはサイレンスされています。" yourAccountSuspendedTitle: "アカウントが凍結されています" @@ -608,7 +604,7 @@ testEmail: "配信テスト" wordMute: "ワードミュート" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" -instanceMute: "インスタンスミュート" +instanceMute: "サーバーミュート" userSaysSomething: "{name}が何かを言いました" makeActive: "アクティブにする" display: "表示" @@ -639,15 +635,15 @@ abuseReported: "内容が送信されました。ご報告ありがとうござ reporter: "通報者" reporteeOrigin: "通報先" reporterOrigin: "通報元" -forwardReport: "リモートインスタンスに通報を転送する" -forwardReportIsAnonymous: "リモートインスタンスからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。" +forwardReport: "リモートサーバーに通報を転送する" +forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。" send: "送信" abuseMarkAsResolved: "対応済みにする" openInNewTab: "新しいタブで開く" openInSideView: "サイドビューで開く" defaultNavigationBehaviour: "デフォルトのナビゲーション" editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。" -instanceTicker: "ノートのインスタンス情報" +instanceTicker: "ノートのサーバー情報" waitingFor: "{x}を待っています" random: "ランダム" system: "システム" @@ -736,7 +732,7 @@ capacity: "容量" inUse: "使用中" editCode: "コードを編集" apply: "適用" -receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る" +receiveAnnouncementFromInstance: "サーバーからのお知らせを受け取る" emailNotification: "メール通知" publish: "公開" inChannelSearch: "チャンネル内検索" @@ -764,7 +760,7 @@ active: "アクティブ" offline: "オフライン" notRecommended: "非推奨" botProtection: "Botプロテクション" -instanceBlocking: "インスタンスブロック" +instanceBlocking: "サーバーブロック" selectAccount: "アカウントを選択" switchAccount: "アカウントを切り替え" enabled: "有効" @@ -784,6 +780,7 @@ popularPosts: "人気の投稿" shareWithNote: "ノートで共有" ads: "広告" expiration: "期限" +startingperiod: "開始期間" memo: "メモ" priority: "優先度" high: "高" @@ -816,6 +813,7 @@ lastCommunication: "直近の通信" resolved: "解決済み" unresolved: "未解決" breakFollow: "フォロワーを解除" +breakFollowConfirm: "フォロワー解除しますか?" itsOn: "オンになっています" itsOff: "オフになっています" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする" @@ -835,8 +833,6 @@ deleteAccountConfirm: "アカウントが削除されます。よろしいです incorrectPassword: "パスワードが間違っています。" voteConfirm: "「{choice}」に投票しますか?" hide: "隠す" -leaveGroup: "グループから抜ける" -leaveGroupConfirm: "「{name}」から抜けますか?" useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示" welcomeBackWithName: "おかえりなさい、{name}さん" clickToFinishEmailVerification: "[{ok}]を押して、メールアドレスの確認を完了してください。" @@ -848,20 +844,24 @@ themeColor: "テーマカラー" size: "サイズ" numberOfColumn: "列の数" searchByGoogle: "検索" -instanceDefaultLightTheme: "インスタンスデフォルトのライトテーマ" -instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ" +instanceDefaultLightTheme: "サーバーデフォルトのライトテーマ" +instanceDefaultDarkTheme: "サーバーデフォルトのダークテーマ" instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。" mutePeriod: "ミュートする期限" +period: "期限" indefinitely: "無期限" tenMinutes: "10分" oneHour: "1時間" oneDay: "1日" oneWeek: "1週間" +oneMonth: "1ヶ月" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" failedToFetchAccountInformation: "アカウント情報の取得に失敗しました" rateLimitExceeded: "レート制限を超えました" cropImage: "画像のクロップ" cropImageAsk: "画像をクロップしますか?" +cropYes: "クロップする" +cropNo: "そのまま使う" file: "ファイル" recentNHours: "直近{n}時間" recentNDays: "直近{n}日" @@ -898,7 +898,7 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" beta: "ベータ" enableAutoSensitive: "自動NSFW判定" -enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、インスタンスによっては自動で設定されることがあります。" +enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。" navbar: "ナビゲーションバー" shuffle: "シャッフル" @@ -908,7 +908,7 @@ pushNotification: "プッシュ通知" subscribePushNotification: "プッシュ通知を有効化" unsubscribePushNotification: "プッシュ通知を停止する" pushNotificationAlreadySubscribed: "プッシュ通知は有効です" -pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に非対応" +pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応" sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する" sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。" windowMaximize: "最大化" @@ -942,6 +942,19 @@ selectFromPresets: "プリセットから選択" achievements: "実績" gotInvalidResponseError: "サーバーの応答が無効です" gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。" +thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります。" +thisPostMayBeAnnoyingHome: "ホームに投稿" +thisPostMayBeAnnoyingCancel: "やめる" +thisPostMayBeAnnoyingIgnore: "このまま投稿" +collapseRenotes: "見たことのあるRenoteを省略して表示" +internalServerError: "サーバー内部エラー" +internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。" +copyErrorInfo: "エラー情報をコピー" +joinThisServer: "このサーバーに登録する" +exploreOtherServers: "他のサーバーを探す" +letsLookAtTimeline: "タイムラインを見てみる" +disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。" +invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。" _achievements: earnedAt: "獲得日時" @@ -1133,7 +1146,7 @@ _achievements: description: "ホームタイムラインの流速が20npmを越す" _viewInstanceChart: title: "アナリスト" - description: "インスタンスのチャートを表示した" + description: "サーバーのチャートを表示した" _outputHelloWorldOnScratchpad: title: "Hello, world!" description: "スクラッチパッドで hello world を出力した" @@ -1170,7 +1183,7 @@ _achievements: _loggedInOnNewYearsDay: title: "あけましておめでとうございます" description: "元日にログインした" - flavor: "今年も弊インスタンスをよろしくお願いします" + flavor: "今年も弊サーバーをよろしくお願いします" _cookieClicked: title: "クッキーをクリックするゲーム" description: "クッキーをクリックした" @@ -1186,7 +1199,7 @@ _role: name: "ロール名" description: "ロールの説明" permission: "ロールの権限" - descriptionOfPermission: "モデレーターは基本的なモデレーションに関する操作を行えます。\n管理者はインスタンスの全ての設定を変更できます。" + descriptionOfPermission: "モデレーターは基本的なモデレーションに関する操作を行えます。\n管理者はサーバーの全ての設定を変更できます。" assignTarget: "アサイン" descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれるかを手動で管理します。\nコンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。" manual: "マニュアル" @@ -1214,7 +1227,7 @@ _role: gtlAvailable: "グローバルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" - canInvite: "インスタンス招待コードの発行" + canInvite: "サーバー招待コードの発行" canManageCustomEmojis: "カスタム絵文字の管理" driveCapacity: "ドライブ容量" pinMax: "ノートのピン留めの最大数" @@ -1283,7 +1296,7 @@ _ad: _forgotPassword: enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" ifNoEmail: "メールアドレスを登録していない場合は、管理者までお問い合わせください。" - contactAdmin: "このインスタンスではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。" + contactAdmin: "このサーバーではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。" _gallery: my: "自分の投稿" @@ -1343,73 +1356,6 @@ _nsfw: ignore: "閲覧注意のメディアを隠さない" force: "常にメディアを隠す" -_mfm: - cheatSheet: "MFMチートシート" - intro: "MFMは、Misskey内の様々な場所で使用できる専用のマークアップ言語です。ここでは、MFMで使用可能な構文一覧が確認できます。" - dummy: "MisskeyでFediverseの世界が広がります" - mention: "メンション" - mentionDescription: "アットマーク + ユーザー名で、特定のユーザーを示すことができます。" - hashtag: "ハッシュタグ" - hashtagDescription: "ナンバーサイン + タグで、ハッシュタグを示すことができます。" - url: "URL" - urlDescription: "URLを示すことができます。" - link: "リンク" - linkDescription: "文章の特定の範囲を、URLに紐づけることができます。" - bold: "太字" - boldDescription: "文字を太く表示して強調することができます。" - small: "目立たなく" - smallDescription: "内容を小さく・薄く表示させることができます。" - center: "中央寄せ" - centerDescription: "内容を中央寄せで表示させることができます。" - inlineCode: "コード(インライン)" - inlineCodeDescription: "プログラムなどのコードをインラインでシンタックスハイライトします。" - blockCode: "コード(ブロック)" - blockCodeDescription: "複数行のプログラムなどのコードをブロックでシンタックスハイライトします。" - inlineMath: "数式(インライン)" - inlineMathDescription: "数式(KaTeX)をインラインで表示します。" - blockMath: "数式(ブロック)" - blockMathDescription: "複数行の数式(KaTeX)をブロックで表示します。" - quote: "引用" - quoteDescription: "内容が引用であることを示すことができます。" - emoji: "カスタム絵文字" - emojiDescription: "コロンでカスタム絵文字名を囲むと、カスタム絵文字を表示させることができます。" - search: "検索" - searchDescription: "入力済み検索ボックスを表示させることができます。" - flip: "反転" - flipDescription: "内容を上下または左右に反転させます。" - jelly: "アニメーション(びよんびよん)" - jellyDescription: "びよんびよんするアニメーションを与えます。" - tada: "アニメーション(じゃーん)" - tadaDescription: "ジャーン!という感じのアニメーションを与えます。" - jump: "アニメーション(ジャンプ)" - jumpDescription: "飛び跳ねるようなアニメーションを与えます。" - bounce: "アニメーション(バウンド)" - bounceDescription: "ぽよんぽよん弾むようなアニメーションを与えます。" - shake: "アニメーション(ぶるぶる)" - shakeDescription: "ぶるぶる震えるアニメーションを与えます。" - twitch: "アニメーション(ブレ)" - twitchDescription: "激しくブレるアニメーションを与えます。" - spin: "アニメーション(回転)" - spinDescription: "回転するアニメーションを与えます。" - x2: "大きく" - x2Description: "内容を大きく表示します。" - x3: "とても大きく" - x3Description: "内容をとても大きく表示します。" - x4: "究極に大きく" - x4Description: "内容を究極に大きく表示します。" - blur: "ぼかし" - blurDescription: "内容をぼかすことができます。ポインターを上に乗せるとはっきり見えるようになります。" - font: "フォント" - fontDescription: "内容のフォントを指定することができます。" - rainbow: "レインボー" - rainbowDescription: "内容をレインボーにします。" - sparkle: "キラキラ" - sparkleDescription: "キラキラしたパーティクルのエフェクトを追加します。" - rotate: "回転" - rotateDescription: "指定した角度で回転させます。" - plain: "プレーン" - plainDescription: "内側の構文を全て無効にします。" - _instanceTicker: none: "表示しない" remote: "リモートユーザーに表示" @@ -1448,10 +1394,10 @@ _wordMute: mutedNotes: "ミュートされたノート" _instanceMute: - instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートします。" + instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。" instanceMuteDescription2: "改行で区切って設定します" - title: "設定したインスタンスのノートを隠します。" - heading: "ミュートするインスタンス" + title: "設定したサーバーのノートを隠します。" + heading: "ミュートするサーバー" _theme: explore: "テーマを探す" @@ -1548,6 +1494,7 @@ _ago: weeksAgo: "{n}週間前" monthsAgo: "{n}ヶ月前" yearsAgo: "{n}年前" + invalid: "ありません" _time: second: "秒" @@ -1584,14 +1531,29 @@ _tutorial: _2fa: alreadyRegistered: "既に設定は完了しています。" - registerDevice: "デバイスを登録" - registerKey: "キーを登録" + registerTOTP: "認証アプリの設定を開始" + passwordToTOTP: "パスワードを入力してください" step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。" step2: "次に、表示されているQRコードをアプリでスキャンします。" - step2Url: "デスクトップアプリでは次のURLを入力します:" - step3: "アプリに表示されているトークンを入力して完了です。" - step4: "これからログインするときも、同じようにトークンを入力します。" - securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーもしくは端末の指紋認証やPINを使用してログインするように設定できます。" + step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。" + step2Url: "デスクトップアプリでは次のURIを入力します:" + step3Title: "確認コードを入力" + step3: "アプリに表示されている確認コード(トークン)を入力して完了です。" + step4: "これからログインするときも、同じように確認コードを入力します。" + securityKeyNotSupported: "お使いのブラウザはセキュリティキーに対応していません。" + registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するには、まず認証アプリの設定を行なってください。" + securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキー、端末の生体認証やPINロック、パスキーといった、WebAuthn由来の鍵を登録します。" + chromePasskeyNotSupported: "Chromeのパスキーは現在サポートしていません。" + registerSecurityKey: "セキュリティキー・パスキーを登録する" + securityKeyName: "キーの名前を入力" + tapSecurityKey: "ブラウザの指示に従い、セキュリティキーやパスキーを登録してください" + removeKey: "セキュリティキーを削除" + removeKeyConfirm: "{name}を削除しますか?" + whyTOTPOnlyRenew: "セキュリティキーが登録されている場合、認証アプリの設定は解除できません。" + renewTOTP: "認証アプリを再設定" + renewTOTPConfirm: "今までの認証アプリの確認コードは使用できなくなります" + renewTOTPOk: "再設定する" + renewTOTPCancel: "やめておく" _permissions: "read:account": "アカウントの情報を見る" @@ -1628,19 +1590,21 @@ _permissions: "write:gallery-likes": "ギャラリーのいいねを操作する" _auth: + shareAccessTitle: "アプリへのアクセス許可" shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?" shareAccessAsk: "アカウントへのアクセスを許可しますか?" + permission: "{name}は次の権限を要求しています" permissionAsk: "このアプリは次の権限を要求しています" pleaseGoBack: "アプリケーションに戻ってやっていってください" callback: "アプリケーションに戻っています" denied: "アクセスを拒否しました" + pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。" _antennaSources: all: "全てのノート" homeTimeline: "フォローしているユーザーのノート" users: "指定した一人または複数のユーザーのノート" userList: "指定したリストのユーザーのノート" - userGroup: "指定したグループのユーザーのノート" _weekday: sunday: "日曜日" @@ -1653,7 +1617,7 @@ _weekday: _widgets: profile: "プロフィール" - instanceInfo: "インスタンス情報" + instanceInfo: "サーバー情報" memo: "付箋" notifications: "通知" timeline: "タイムライン" @@ -1667,7 +1631,7 @@ _widgets: digitalClock: "デジタル時計" unixClock: "UNIX時計" federation: "連合" - instanceCloud: "インスタンスクラウド" + instanceCloud: "サーバークラウド" postForm: "投稿フォーム" slideshow: "スライドショー" button: "ボタン" @@ -1720,8 +1684,8 @@ _visibility: followersDescription: "自分のフォロワーのみに公開" specified: "ダイレクト" specifiedDescription: "指定したユーザーのみに公開" - localOnly: "ローカルのみ" - localOnlyDescription: "リモートユーザーには非公開" + disableFederation: "連合なし" + disableFederationDescription: "他サーバーへの配信を行いません" _postForm: replyPlaceholder: "このノートに返信..." @@ -1870,12 +1834,9 @@ _notification: youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" youRenoted: "{name}がRenoteしました" - youGotMessagingMessageFromUser: "{name}からのチャットがあります" - youGotMessagingMessageFromGroup: "{name}のチャットがあります" youWereFollowed: "フォローされました" youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" - youWereInvitedToGroup: "{userName}があなたをグループに招待しました" pollEnded: "アンケートの結果が出ました" unreadAntennaNote: "アンテナ {name}" emptyPushNotificationMessage: "プッシュ通知の更新をしました" @@ -1892,7 +1853,7 @@ _notification: pollEnded: "アンケートが終了" receiveFollowRequest: "フォロー申請を受け取った" followRequestAccepted: "フォローが受理された" - groupInvited: "グループに招待された" + achievementEarned: "実績の獲得" app: "連携アプリからの通知" _actions: @@ -1928,3 +1889,7 @@ _deck: channel: "チャンネル" mentions: "あなた宛て" direct: "ダイレクト" + +_dialog: + charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}" + charactersBelow: "最小文字数を下回っています! 現在 {current} / 制限 {min}" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 05de911bec..862453dd51 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -46,7 +46,7 @@ copyContent: "内容をコピー" copyLink: "リンクをコピー" delete: "ほかす" deleteAndEdit: "ほかして直す" -deleteAndEditConfirm: "このノートをほかして書き直すんか?このノートへのリアクション、Renote、返信も全部消えてまうで。" +deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのリアクション、Renote、返信も全部消えるんやけどそれでもええん?" addToList: "リストに入れたる" sendMessage: "メッセージを送る" copyRSS: "RSSをコピー" @@ -67,7 +67,7 @@ import: "インポート" export: "エクスポート" files: "ファイル" download: "ダウンロード" -driveFileDeleteConfirm: "ファイル「{name}」を消してしもうてええか?このファイルを添付したノートも消えてまうで。" +driveFileDeleteConfirm: "ファイル「{name}」をほかしてええか?このファイルを添付したノートも消えてまうで。" unfollowConfirm: "{name}のフォローを解除してもええんか?" exportRequested: "エクスポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。エクスポート終わったら「ドライブ」に突っ込んどくで。" importRequested: "インポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。" @@ -83,13 +83,13 @@ manageLists: "リストの管理" error: "エラー" somethingHappened: "なんかアカンことが起こったで" retry: "もっぺんやる?" -pageLoadError: "ページの読み込みに失敗してしもうたで…" +pageLoadError: "ページの読み込みに失敗してもうたわ…" pageLoadErrorDescription: "これは普通、ネットワークかブラウザキャッシュが原因やからね。キャッシュをクリアするか、もうちっとだけ待ってくれへんか?" serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。" youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。" enterListName: "リスト名を入れてや" privacy: "プライバシー" -makeFollowManuallyApprove: "自分が認めた人だけがこのアカウントをフォローできるようにする" +makeFollowManuallyApprove: "他人のフォローは許可してからや!" defaultNoteVisibility: "もとからの公開範囲" follow: "フォロー" followRequest: "フォローを頼む" @@ -103,6 +103,8 @@ renoted: "Renoteしたで。" cantRenote: "この投稿はRenoteできへんらしい。" cantReRenote: "Renote自体はRenoteできへんで。" quote: "引用" +inChannelRenote: "チャンネル内Renote" +inChannelQuote: "チャンネル内引用" pinnedNote: "ピン留めされとるノート" pinned: "ピン留めしとく" you: "あんた" @@ -129,6 +131,7 @@ unblockConfirm: "ブロックやめたるってほんまか?" suspendConfirm: "凍結してしもうてええか?" unsuspendConfirm: "解凍するけどええか?" selectList: "リストを選ぶ" +selectChannel: "チャンネルを選ぶ" selectAntenna: "アンテナを選ぶ" selectWidget: "ウィジェットを選ぶ" editWidgets: "ウィジェットをいじる" @@ -150,7 +153,7 @@ flagShowTimelineReplies: "タイムラインにノートへの返信を表示す flagShowTimelineRepliesDescription: "オンにしたら、タイムラインにユーザーのノートの他にもそのユーザーの他のノートへの返信を表示するで。" autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく" addAccount: "アカウントを追加" -loginFailed: "ログインに失敗してしもうた…" +loginFailed: "ログインに失敗してもうた…" showOnRemote: "リモートで見る" general: "全般" wallpaper: "壁紙" @@ -242,7 +245,7 @@ resetAreYouSure: "リセットしてええん?" saved: "保存したで!" messaging: "チャット" upload: "アップロード" -keepOriginalUploading: "オリジナル画像を保持するで" +keepOriginalUploading: "オリジナル画像を保持するわ" keepOriginalUploadingDescription: "画像を上げるときにオリジナル版を保持するで。オフにしたら上げたときにブラウザでWeb公開用の画像を生成するで。 " fromDrive: "ドライブから" fromUrl: "URLから" @@ -256,6 +259,8 @@ noMoreHistory: "これより過去の履歴はあらへんで" startMessaging: "チャットやるで" nUsersRead: "{n}人が読んでもうた" agreeTo: "{0}に同意したで" +agreeBelow: "下記に同意したる" +basicNotesBeforeCreateAccount: "よう読んでやってや" tos: "利用規約" start: "始める" home: "ホーム" @@ -300,7 +305,7 @@ avatar: "アイコン" banner: "バナー" nsfw: "閲覧注意" whenServerDisconnected: "サーバーとの接続が切れたとき" -disconnectedFromServer: "サーバーとの通信が切れたで" +disconnectedFromServer: "サーバーが機嫌悪いねん" reload: "リロード" doNothing: "何もせんとく" reloadConfirm: "リロードしてええか?" @@ -388,16 +393,19 @@ about: "情報" aboutMisskey: "Misskeyってなんや?" administrator: "管理者" token: "トークン" -twoStepAuthentication: "二段階認証" +2fa: "二要素認証" +totp: "認証アプリ" +totpDescription: "認証アプリ使てワンタイムパスワードを入れる" moderator: "モデレーター" moderation: "モデレーション" nUsersMentioned: "{n}人が投稿" +securityKeyAndPasskey: "セキュリティキー・パスキー" securityKey: "セキュリティキー" -securityKeyName: "キーの名前" -registerSecurityKey: "セキュリティキーを登録するで" lastUsed: "最後につこうた日" +lastUsedAt: "最後に使たん: {t}" unregister: "登録やめる" passwordLessLogin: "パスワード無くてもログインできるようにする" +passwordLessLoginDescription: "パスワードやなくて、セキュリティキーとかパスキーだけでログインするわ" resetPassword: "パスワードをリセット" newPasswordIs: "今度のパスワードは「{password}」や" reduceUiAnimation: "UIの動きやアニメーションを減らす" @@ -412,24 +420,15 @@ markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ" help: "ヘルプ" inputMessageHere: "ここにメッセージ書いてや" close: "閉じる" -group: "グループ" -groups: "グループ" -createGroup: "グループを作るで" -ownedGroups: "所有しとるグループ" -joinedGroups: "参加しとるグループ" invites: "来てや" -groupName: "グループ名" members: "メンバー" transfer: "譲渡" -messagingWithUser: "ユーザーとチャット" -messagingWithGroup: "グループでチャット" title: "タイトル" text: "テキスト" enable: "有効にするで" next: "次" retype: "もっかい入力" noteOf: "{user}のノート" -inviteToGroup: "グループに招く" quoteAttached: "引用付いとるで" quoteQuestion: "引用として添付してもええか?" noMessagesYet: "まだチャットはあらへんで" @@ -451,20 +450,18 @@ passwordMatched: "よし!一致や!" passwordNotMatched: "一致しとらんで?" signinWith: "{x}でログイン" signinFailed: "ログインできんかったで。もっかいユーザー名とパスワードを確認してみてな。" -tapSecurityKey: "セキュリティキーにタッチしてな" or: "それか" language: "言語" uiLanguage: "UIの表示言語" -groupInvited: "グループに招待されとるで" aboutX: "{x}について" emojiStyle: "絵文字のスタイル" native: "ネイティブ" disableDrawer: "メニューをドロワーで表示せぇへん" -youHaveNoGroups: "グループがあらへんねぇ。" -joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループ作ってからやってな" +showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで" noHistory: "履歴はあらへんねぇ。" signinHistory: "ログイン履歴" -disableAnimatedMfm: "動きがやかましいMFMを止める" +enableAdvancedMfm: "ややこしいMFMもありにする" +enableAnimatedMfm: "動きがやかましいMFMも許したる" doing: "やっとるがな" category: "カテゴリ" tags: "タグ" @@ -585,7 +582,7 @@ generateAccessToken: "アクセストークンの発行" permission: "権限" enableAll: "全部使えるようにする" disableAll: "全部使えへんようにする" -tokenRequested: "アカウントへのアクセス許可" +tokenRequested: "アカウントへのアクセス許してやったらどうや" pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。" notificationType: "通知の種類" edit: "編集" @@ -673,8 +670,8 @@ sentReactionsCount: "リアクションした数やで" receivedReactionsCount: "リアクションされた数" pollVotesCount: "アンケートに投票した数" pollVotedCount: "アンケートに投票された数" -yes: "はい" -no: "いいえ" +yes: "ええで" +no: "あかんで" driveFilesCount: "ドライブのファイル数" driveUsage: "ドライブ使用量やで" noCrawle: "クローラーによるインデックスを拒否するで" @@ -783,6 +780,7 @@ popularPosts: "人気の投稿" shareWithNote: "ノートで共有" ads: "広告" expiration: "期限" +startingperiod: "始めた期間" memo: "メモ" priority: "優先度" high: "高い" @@ -815,6 +813,7 @@ lastCommunication: "直近の通信" resolved: "解決したで" unresolved: "まだ解決してないで" breakFollow: "フォロワーを解除するで" +breakFollowConfirm: "フォロワー解除してもええか?" itsOn: "オンになっとるよ" itsOff: "オフになってるで" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで" @@ -834,10 +833,8 @@ deleteAccountConfirm: "アカウントを消すで?ええんか?" incorrectPassword: "パスワードがちゃうで。" voteConfirm: "「{choice}」に投票するんか?" hide: "隠す" -leaveGroup: "グループから抜けるで" -leaveGroupConfirm: "「{name}」から抜けるん?" useDrawerReactionPickerForMobile: "ケータイとかのときドロワーで表示するで" -welcomeBackWithName: "まいど、{name}さん" +welcomeBackWithName: "まいど、{name}はん" clickToFinishEmailVerification: "[{ok}]を押してメアドの確認を終わらせてなー" overridedDeviceKind: "デバイスタイプ" smartphone: "スマホ" @@ -851,16 +848,20 @@ instanceDefaultLightTheme: "インスタンスの最初の明るいテーマ" instanceDefaultDarkTheme: "インスタンスの最初の暗いテーマ" instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。" mutePeriod: "ミュートする期間" +period: "期限" indefinitely: "無期限" tenMinutes: "10分" oneHour: "1時間" oneDay: "1日" oneWeek: "1週間" +oneMonth: "1ヶ月" reflectMayTakeTime: "反映されるまで時間がかかることがあるで" failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…" rateLimitExceeded: "レート制限が超えたみたいやで" cropImage: "画像のクロップ" cropImageAsk: "画像をクロップしたってええか?" +cropYes: "切り抜いたる" +cropNo: "切り抜かへん" file: "ファイル" recentNHours: "直近{n}時間" recentNDays: "直近{n}日" @@ -892,7 +893,7 @@ fast: "速い" sensitiveMediaDetection: "センシティブなメディアの検出" localOnly: "ローカルのみ" remoteOnly: "リモートのみ" -failedToUpload: "アップロードに失敗したで" +failedToUpload: "アップロードに失敗してもうたわ…" cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたでアップロードできまへん。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いでアップロードできまへん。" beta: "ベータ" @@ -938,6 +939,258 @@ cannotPerformTemporary: "一時的に利用できへんで" cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。" preset: "プリセット" selectFromPresets: "プリセットから選ぶ" +achievements: "実績" +gotInvalidResponseError: "サーバー黙っとるわ、知らんけど" +gotInvalidResponseErrorDescription: "サーバーいま日曜日。またきて月曜日。" +thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。" +thisPostMayBeAnnoyingHome: "ホームに投稿" +thisPostMayBeAnnoyingCancel: "やめとく" +thisPostMayBeAnnoyingIgnore: "このまま投稿" +collapseRenotes: "見たことあるRenoteは省略やで" +internalServerError: "サーバー内部エラー" +internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ" +copyErrorInfo: "エラー情報をコピー" +joinThisServer: "このサーバーに登録するわ" +exploreOtherServers: "他のサーバー見てみる" +letsLookAtTimeline: "タイムライン見てみーや" +disableFederationWarn: "連合が無効になっとるで。無効にしても投稿は非公開ってわけちゃうねん。大体の場合はこのオプションを有効にする必要は別にないで。" +invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。" +_achievements: + earnedAt: "貰った日ぃ" + _types: + _notes1: + title: "まいど!" + description: "初めてノート投稿したった" + flavor: "Misskeyを楽しんでな~" + _notes10: + title: "ノートの天保山" + description: "ノートを10回投稿した" + _notes100: + title: "ノートの真田山" + description: "ノートを100回投稿した" + _notes500: + title: "ノートの生駒山" + description: "ノートを500回投稿した" + _notes1000: + title: "ノートの山" + description: "ノートを1,000回投稿した" + _notes5000: + title: "箕面の滝からノート" + description: "ノートを5,000回投稿した" + _notes10000: + title: "スーパーノート" + description: "ノートを10,000回投稿した" + _notes20000: + title: "ニードモアノート" + description: "ノートを20,000回投稿した" + _notes30000: + title: "ノートノートノート" + description: "ノートを30,000回投稿した" + _notes40000: + title: "ノート工場" + description: "ノートを40,000回投稿した" + _notes50000: + title: "ノートの惑星" + description: "ノートを50,000回投稿した" + _notes60000: + title: "ノートクエーサー" + description: "ノートを60,000回投稿した" + _notes70000: + title: "ブラックノートホール" + description: "ノートを70,000回投稿した" + _notes80000: + title: "ノートギャラクシー" + description: "ノートを80,000回投稿した" + _notes90000: + title: "ノートバース" + description: "ノートを90,000回投稿した" + _notes100000: + title: "ALL YOUR NOTE ARE BELONG TO US" + description: "ノートを100,000回投稿した" + flavor: "そんなに書くことあるんか?" + _login3: + title: "ビギナーⅠ" + description: "通算3日ログインした" + flavor: "今日からワシはミスキストやで" + _login7: + title: "ビギナーⅡ" + description: "通算7日ログインした" + flavor: "慣れてきたんちゃう?" + _login15: + title: "ビギナーⅢ" + description: "通算15日ログインした" + _login30: + title: "ミスキストⅠ" + description: "通算30日ログインした" + _login60: + title: "ミスキストⅡ" + description: "通算60日ログインした" + _login100: + title: "ミスキストⅢ" + description: "通算100日ログインした" + flavor: "そのユーザー、ミスキストにつき" + _login200: + title: "常連さんⅠ" + description: "通算200日ログインした" + _login300: + title: "常連さんⅡ" + description: "通算300日ログインした" + _login400: + title: "常連さんⅢ" + description: "通算400日ログインした" + _login500: + title: "ベテランさんⅠ" + description: "通算500日ログインした" + flavor: "あんたら、うちはノートが好きや" + _login600: + title: "ベテランさんⅡ" + description: "通算600日ログインした" + _login700: + title: "ベテランさんⅢ" + description: "通算700日ログインした" + _login800: + title: "ノートマイスターⅠ" + description: "通算800日ログインした" + _login900: + title: "ノートマイスターⅡ" + description: "通算900日ログインした" + _login1000: + title: "ノートマイスターⅢ" + description: "通算1,000日ログインした" + flavor: "Misskeyようさん使てもろておおきにな!" + _noteClipped1: + title: "アカンどれもクリップしたいわ" + description: "初めてノートをクリップした" + _noteFavorited1: + title: "星ぃみるひと" + description: "初めてノートをお気に入りに登録した" + _myNoteFavorited1: + title: "星ぃ欲しい" + description: "ワレのノートが他のひとにお気に入り登録されたで" + _profileFilled: + title: "準備万端や" + description: "プロフィールを設定した" + _markedAsCat: + title: "吾輩は猫やねん" + description: "アカウントがCatになってもうた" + flavor: "名前はまだないねん。" + _following1: + title: "はじめてのフォロー" + description: "初めてフォローした" + _following10: + title: "ついてく、ついてく" + description: "フォローが10人超えた" + _following50: + title: "友達ぎょうさん" + description: "フォローが50人超えた" + _following100: + title: "友達100人" + description: "フォローが100人超えた" + _following300: + title: "いや友達多すぎやろ" + description: "フォローが300人超えた" + _followers1: + title: "はじめてのフォロワー" + description: "初めてフォローされた" + _followers10: + title: "フォローみぃ!" + description: "フォロワーが10人超えた" + _followers50: + title: "ぞろぞろ" + description: "フォロワーが50人超えた" + _followers100: + title: "人気もん" + description: "フォロワーが100人超えた" + _followers300: + title: "ほらそこ一列に並んで!" + description: "フォロワーが300人超えた" + _followers500: + title: "基地局" + description: "フォロワーが500人超えた" + _followers1000: + title: "インフルエンサー" + description: "フォロワーが1,000人超えた" + _collectAchievements30: + title: "実績コレクター" + description: "実績を30個以上獲得した" + _viewAchievements3min: + title: "実績好き" + description: "実績一覧を3分以上眺め続けた" + _iLoveMisskey: + title: "Misskey好きやねん" + description: "\"I ❤ #Misskey\"を投稿した" + flavor: "Misskeyを使ってくれてありがとうな~ by 開発チーム" + _foundTreasure: + title: "なんでも鑑定団" + description: "隠されたお宝を発見した" + _client30min: + title: "ねんね" + description: "クライアントを起動してから30分以上経過した" + _noteDeletedWithin1min: + title: "*おおっと*" + description: "投稿してから1分以内にその投稿を消した" + _postedAtLateNight: + title: "夜行性" + description: "深夜にノートを投稿した" + flavor: "そろそろ寝よか" + _postedAt0min0sec: + title: "時報" + description: "0分0秒にノートを投稿した" + flavor: "ポッ ポッ ポッ ピーン" + _selfQuote: + title: "自己言及" + description: "自分のノートを引用した" + _htl20npm: + title: "流れるTL" + description: "ホームタイムラインの流速が20npmを超す" + _viewInstanceChart: + title: "アナリスト" + description: "インスタンスのチャートを表示した" + _outputHelloWorldOnScratchpad: + title: "Hello, world!" + description: "スクラッチパッドで hello worldを出力した" + _open3windows: + title: "マド開けすぎ" + description: "ウィンドウを3つ以上開いた状態にした" + _driveFolderCircularReference: + title: "環状線" + description: "ドライブのフォルダを再帰的な入れ子にしようとした" + _reactWithoutRead: + title: "ちゃんと読んだんか?" + description: "100文字以上のテキストを含むノートに投稿されてから3秒以内にリアクションした" + _clickedClickHere: + title: "ここをクリック" + description: "ここをクリックした" + _justPlainLucky: + title: "単なるラッキー" + description: "10秒ごとに0.005%の確率で獲得" + _setNameToSyuilo: + title: "神様コンプレックス" + description: "名前を syuilo に設定した" + _passedSinceAccountCreated1: + title: "一周年" + description: "アカウント作成から1年経過した" + _passedSinceAccountCreated2: + title: "二周年" + description: "アカウント作成から2年経過した" + _passedSinceAccountCreated3: + title: "三周年" + description: "アカウント作成から3年経過した" + _loggedInOnBirthday: + title: "ハッピーバースデー!" + description: "誕生日にログインした" + _loggedInOnNewYearsDay: + title: "あけましておめでとうございます!" + description: "元旦にログインした" + flavor: "今年も弊インスタンスをよろしくお願いします" + _cookieClicked: + title: "クッキー叩くやつ" + description: "クッキー叩いてもうた" + flavor: "兄ちゃんソフト間違っとんで" + _brainDiver: + title: "Brain Diver" + description: "Brain Diverへのリンクを投稿したった" + flavor: "Misskey-Misskey La-Tu-Ma" _role: new: "ロールの作成" edit: "ロールの編集" @@ -958,6 +1211,9 @@ _role: baseRole: "ベースロール" useBaseValue: "ベースロールの値を使用" chooseRoleToAssign: "アサインするロールを選択" + iconUrl: "アイコン画像のURL" + asBadge: "バッジとして見せる" + descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。" canEditMembersByModerator: "モデレーターのメンバー編集を許可" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。" priority: "優先度" @@ -1083,72 +1339,6 @@ _nsfw: respect: "閲覧注意のメディアは隠すで" ignore: "閲覧注意のメディアは隠さへんで" force: "常にメディアを隠すで" -_mfm: - cheatSheet: "MFMチートシート" - intro: "MFMは、Misskey内の色んな所で使える専用のマークアップ言語やで。このページでMFMで使える構文一覧が確認できるで。" - dummy: "MisskeyでFediverseの世界が広がります" - mention: "メンション" - mentionDescription: "アットマーク + ユーザー名で、特定のユーザーを示すことができるで。" - hashtag: "ハッシュタグ" - hashtagDescription: "ナンバーサイン + タグで、ハッシュタグを示すことができるで。" - url: "URL" - urlDescription: "URLを示すことができるで。" - link: "リンク" - linkDescription: "文章の特定の範囲をURLに紐づけることができるで" - bold: "太字" - boldDescription: "文字を太く表示して強調することができるで" - small: "目立たなく" - smallDescription: "内容を小さく・薄く表示することができるで" - center: "中央寄せ" - centerDescription: "内容を中央寄せで表示することができるで" - inlineCode: "コード(インライン)" - inlineCodeDescription: "プログラムとかのコードをインラインでシンタックスハイライトするで" - blockCode: "コード(ブロック)" - blockCodeDescription: "複数行のプログラムとかのコードをブロックでシンタックスハイライトするで" - inlineMath: "数式(インライン)" - inlineMathDescription: "数式(KaTeX)をインラインで表示するで" - blockMath: "数式(ブロック)" - blockMathDescription: "複数行の数式(KaTeX)をブロックで表示するで" - quote: "引用" - quoteDescription: "内容が引用ってことを示すことができるで" - emoji: "カスタム絵文字" - emojiDescription: "コロンでカスタム絵文字名を囲んだると、カスタム絵文字を表示させることができるで" - search: "探す" - searchDescription: "入力済み検索ボックスを表示することができるで" - flip: "反転" - flipDescription: "内容を上下または左右に反転するで" - jelly: "アニメーション(びよんびよん)" - jellyDescription: "びよんびよんするアニメーションやな。" - tada: "アニメーション(じゃーん)" - tadaDescription: "ジャーン!ってな感じのアニメーションやな。" - jump: "アニメーション(ジャンプ)" - jumpDescription: "飛び跳ねるようなアニメーションやな。" - bounce: "アニメーション(バウンド)" - bounceDescription: "ぽよんぽよん弾むようなアニメーションやな。" - shake: "アニメーション(ぶるぶる)" - shakeDescription: "ぶるぶる震えるアニメーションやな。" - twitch: "アニメーション(ブレ)" - twitchDescription: "激しくブレるアニメーションやな。" - spin: "アニメーション(回転)" - spinDescription: "回転するアニメーションやな。" - x2: "大きく" - x2Description: "内容を大きく表示するで" - x3: "とても大きく" - x3Description: "内容をとても大きく表示するで" - x4: "究極に大きく" - x4Description: "内容を究極に大きく表示するで" - blur: "ぼかし" - blurDescription: "内容をぼかすことができるで。ポインターを上に乗せるとはっきり見えるようになるで" - font: "フォント" - fontDescription: "内容のフォントを指定することができるで" - rainbow: "レインボー" - rainbowDescription: "内容をレインボーにするで" - sparkle: "キラキラ" - sparkleDescription: "キラキラしたバーティ来るのエフェクトを追加するで" - rotate: "回転" - rotateDescription: "指定した角度で回転させるで" - plain: "プレーン" - plainDescription: "内側の構文を全部無効にするで" _instanceTicker: none: "表示せん" remote: "リモートユーザーに表示" @@ -1278,6 +1468,7 @@ _ago: weeksAgo: "{n}週間前" monthsAgo: "{n}ヶ月前" yearsAgo: "{n}年前" + invalid: "あらへん" _time: second: "秒" minute: "分" @@ -1311,14 +1502,29 @@ _tutorial: step8_3: "通知の設定はあとから変更できるで" _2fa: alreadyRegistered: "もう設定終わっとるわ。" - registerDevice: "デバイスを登録するで" - registerKey: "キーを登録するで" + registerTOTP: "認証アプリの設定はじめる" + passwordToTOTP: "パスワードを入れてーや" step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。" step2: "次に、ここにあるQRコードをアプリでスキャンしてな~。" + step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。" step2Url: "デスクトップアプリやったら次のURLを入力してや:" + step3Title: "確認コードを入れてーや" step3: "アプリに表示されているトークンを入力して終わりや。" step4: "これからログインするときも、同じようにトークンを入力するんやで" + securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。" + registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーか端末の指紋認証やPINを使ってログインするように設定できるで。" + chromePasskeyNotSupported: "Chromeのパスキーは今んとこ対応してないねん。" + registerSecurityKey: "セキュリティキー・パスキーを登録するわ" + securityKeyName: "キーの名前を入れてーや" + tapSecurityKey: "ブラウザが言うこと聞いて、セキュリティキーとかパスキー登録しといでや" + removeKey: "セキュリティキーをほかす" + removeKeyConfirm: "{name}を消すん?" + whyTOTPOnlyRenew: "セキュリティキーが登録されとったら、認証アプリの設定は解除できへんで。" + renewTOTP: "認証アプリをもっかい設定" + renewTOTPConfirm: "今までの人称アプリの確認コードは使えんくなるけどええか?" + renewTOTPOk: "もっかい設定する" + renewTOTPCancel: "やめとく" _permissions: "read:account": "アカウントの情報を見るで" "write:account": "アカウントの情報を変更するで" @@ -1353,18 +1559,20 @@ _permissions: "read:gallery-likes": "ギャラリーのいいねを見るで" "write:gallery-likes": "ギャラリーのいいねを操作するで" _auth: + shareAccessTitle: "アプリへのアクセス許してやったらどうや" shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?" shareAccessAsk: "アカウントのアクセスを許可してもええか?" + permission: "{name}に次の権限つけたってやって" permissionAsk: "このアプリは次の権限を要求しとるで" pleaseGoBack: "アプリケーションに戻ってええよ" callback: "アプリケーションに戻っとるで" denied: "アクセスを拒否ったで" + pleaseLogin: "アプリにアクセスさせるんやったら、ログインしてや。" _antennaSources: all: "みんなのノート" homeTimeline: "フォローしとるユーザーのノート" users: "選らんだ一人か複数のユーザーのノート" userList: "選んだリストのユーザーのノート" - userGroup: "選んだグループのユーザーのノート" _weekday: sunday: "日曜日" monday: "月曜日" @@ -1439,16 +1647,16 @@ _visibility: followersDescription: "自分のフォロワーのみに公開するで" specified: "ダイレクト" specifiedDescription: "選んだユーザーのみに公開するで" - localOnly: "ローカルのみ" - localOnlyDescription: "リモートユーザーには非公開にするで" + disableFederation: "連合なし" + disableFederationDescription: "他インスタンスへは送らんとくわ" _postForm: replyPlaceholder: "このノートに返信..." quotePlaceholder: "このノートを引用..." channelPlaceholder: "チャンネルに投稿..." _placeholders: - a: "いまどうしとるん?" + a: "いまどないしとるん?" b: "何かあったん?" - c: "何を考えとるん?" + c: "何か考えとるん?" d: "何か言いたいことあるん?" e: "ここに書いてーなー" f: "あんたが書くの待っとるで" @@ -1535,7 +1743,7 @@ _pages: viewPage: "ページを見る" like: "ええやん" unlike: "良くないわ" - my: "人気のページ" + my: "自分のページ" liked: "ええと思ったページ" featured: "人気" inspector: "インスペクター" @@ -1578,15 +1786,13 @@ _notification: youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" youRenoted: "{name}がRenoteしたみたいやで" - youGotMessagingMessageFromUser: "{name}からのチャットがあるで" - youGotMessagingMessageFromGroup: "{name}のチャットがあるで" youWereFollowed: "フォローされたで" youReceivedFollowRequest: "フォロー許可してほしいみたいやな" yourFollowRequestAccepted: "フォローさせてもろたで" - youWereInvitedToGroup: "グループに招待されとるで" pollEnded: "アンケートの結果が出たみたいや" unreadAntennaNote: "アンテナ {name}" emptyPushNotificationMessage: "プッシュ通知の更新をしといたで" + achievementEarned: "実績を獲得しとるで" _types: all: "すべて" follow: "フォロー" @@ -1598,7 +1804,7 @@ _notification: pollEnded: "アンケートが終了したで" receiveFollowRequest: "フォロー許可してほしいみたいやで" followRequestAccepted: "フォローが受理されたで" - groupInvited: "グループに招待されたで" + achievementEarned: "実績の獲得" app: "連携アプリからの通知や" _actions: followBack: "フォローバック" @@ -1631,3 +1837,6 @@ _deck: channel: "チャンネル" mentions: "あんた宛て" direct: "ダイレクト" +_dialog: + charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}" + charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}" diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index 7c2e3a065e..18fd8f5a58 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -39,10 +39,8 @@ remove: "Kkes" connectService: "Qqen" userList: "Tibdarin" securityKey: "Tasarutt n tɣellist" -securityKeyName: "Isem n tsarutt" signinRequired: "Ttxil jerred" signinWith: "Tuqqna s {x}" -tapSecurityKey: "Sekcem tasarutt-ik·im n tɣellist" uiLanguage: "Tutlayt n wegrudem" accountSettings: "Iɣewwaṛen n umiḍan" plugins: "Izegrar" @@ -61,10 +59,6 @@ account: "Imiḍan" _email: _follow: title: "Yeṭṭafaṛ-ik·em-id" -_mfm: - mention: "Bder" - search: "Nadi" - font: "Tasefsit" _theme: keys: mention: "Bder" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index 55b72d3a6e..ef66f3fbd2 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -64,8 +64,6 @@ file: "ಕಡತಗಳು" _email: _follow: title: "ಹಿಂಬಾಲಿಸಿದರು" -_mfm: - search: "ಹುಡುಕು" _sfx: notification: "ಅಧಿಸೂಚನೆಗಳು" _widgets: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 23814208a3..9115afe5a5 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -2,7 +2,7 @@ _lang_: "한국어" headlineMisskey: "노트로 연결되는 네트워크" introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응을 추가할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀" -poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼Misskey를 사용한 서비스(Misskey 인스턴스라고 불립니다) 중 하나입니다." +poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼Misskey를 사용한 서버 중 하나입니다." monthAndDay: "{month}월 {day}일" search: "검색" notifications: "알림" @@ -15,10 +15,10 @@ gotIt: "알겠어요" cancel: "취소" noThankYou: "나중에" enterUsername: "유저명 입력" -renotedBy: "{user}님이 리노트" +renotedBy: "{user}님의 리노트" noNotes: "노트가 없습니다" noNotifications: "표시할 알림이 없습니다" -instance: "인스턴스" +instance: "서버" settings: "설정" basicSettings: "기본 설정" otherSettings: "기타 설정" @@ -103,6 +103,8 @@ renoted: "리노트했습니다" cantRenote: "이 게시물은 리노트 할 수 없습니다." cantReRenote: "리노트를 리노트 할 수 없습니다." quote: "인용" +inChannelRenote: "채널 내 리노트" +inChannelQuote: "채널 내 인용" pinnedNote: "고정해놓은 노트" pinned: "프로필에 고정" you: "당신" @@ -129,6 +131,7 @@ unblockConfirm: "이 계정의 차단을 해제하시겠습니까?" suspendConfirm: "이 계정을 정지하시겠습니까?" unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?" selectList: "리스트 선택" +selectChannel: "채널 선택" selectAntenna: "안테나 선택" selectWidget: "위젯 선택" editWidgets: "위젯 편집" @@ -160,13 +163,13 @@ searchWith: "검색: {q}" youHaveNoLists: "리스트가 없습니다" followConfirm: "{name}님을 팔로우 하시겠습니까?" proxyAccount: "프록시 계정" -proxyAccountDescription: "프록시 계정은 특정 조건 하에서 유저의 리모트 팔로우를 대행하는 계정입니다. 예를 들면, 유저가 리모트 유저를 리스트에 넣었을 때, 리스트에 들어간 유저를 아무도 팔로우한 적이 없다면 액티비티가 인스턴스로 배달되지 않기 때문에, 대신 프록시 계정이 해당 유저를 팔로우하도록 합니다." +proxyAccountDescription: "프록시 계정은 특정 조건 하에서 유저의 리모트 팔로우를 대행하는 계정입니다. 예를 들면, 유저가 리모트 유저를 리스트에 넣었을 때, 리스트에 들어간 유저를 아무도 팔로우한 적이 없다면 액티비티가 서버로 배달되지 않기 때문에, 대신 프록시 계정이 해당 유저를 팔로우하도록 합니다." host: "호스트" selectUser: "유저 선택" recipient: "수신인" annotation: "내용에 대한 주석" federation: "연합" -instances: "인스턴스" +instances: "서버" registeredAt: "등록 날짜" latestRequestReceivedAt: "마지막으로 요청을 받은 시간" latestStatus: "마지막 상태" @@ -175,7 +178,7 @@ charts: "차트" perHour: "1시간마다" perDay: "1일마다" stopActivityDelivery: "액티비티 보내지 않기" -blockThisInstance: "이 인스턴스를 차단" +blockThisInstance: "이 서버를 차단" operations: "작업" software: "소프트웨어" version: "버전" @@ -186,15 +189,15 @@ jobQueue: "작업 대기열" cpuAndMemory: "CPU와 메모리" network: "네트워크" disk: "디스크" -instanceInfo: "인스턴스 정보" +instanceInfo: "서버 정보" statistics: "통계" clearQueue: "대기열 비우기" clearQueueConfirmTitle: "대기열을 비우시겠습니까?" clearQueueConfirmText: "대기열에 남아 있는 노트는 더이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다." clearCachedFiles: "캐시 비우기" clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제하시겠습니까?" -blockedInstances: "차단된 인스턴스" -blockedInstancesDescription: "차단하려는 인스턴스의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다." +blockedInstances: "차단된 서버" +blockedInstancesDescription: "차단하려는 서버의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다." muteAndBlock: "뮤트 및 차단" mutedUsers: "뮤트한 유저" blockedUsers: "차단한 유저" @@ -217,9 +220,9 @@ all: "전체" subscribing: "구독 중" publishing: "배포 중" notResponding: "응답 없음" -instanceFollowing: "인스턴스의 팔로잉" -instanceFollowers: "인스턴스의 팔로워" -instanceUsers: "인스턴스의 유저" +instanceFollowing: "서버의 팔로잉" +instanceFollowers: "서버의 팔로워" +instanceUsers: "서버의 유저" changePassword: "비밀번호 변경" security: "보안" retypedNotMatch: "입력이 일치하지 않습니다." @@ -256,6 +259,8 @@ noMoreHistory: "이것보다 과거의 기록이 없습니다" startMessaging: "대화 시작하기" nUsersRead: "{n}명이 읽음" agreeTo: "{0}에 동의" +agreeBelow: "아래 내용에 동의합니다" +basicNotesBeforeCreateAccount: "기본적인 주의사항" tos: "이용 약관" start: "시작하기" home: "홈" @@ -309,8 +314,8 @@ unwatch: "지켜보기 해제" accept: "허가" reject: "거부" normal: "정상" -instanceName: "인스턴스 이름" -instanceDescription: "인스턴스 소개" +instanceName: "서버 이름" +instanceDescription: "서버 소개" maintainerName: "관리자 이름" maintainerEmail: "관리자 이메일" tosUrl: "이용약관 URL" @@ -340,7 +345,7 @@ basicInfo: "기본 정보" pinnedUsers: "고정된 유저" pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다." pinnedPages: "고정한 페이지" -pinnedPagesDescription: "인스턴스의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다." +pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다." pinnedClipId: "고정할 클립의 ID" pinnedNotes: "고정해놓은 노트" hcaptcha: "hCaptcha" @@ -388,16 +393,19 @@ about: "정보" aboutMisskey: "Misskey에 대하여" administrator: "관리자" token: "토큰" -twoStepAuthentication: "2단계 인증" +2fa: "2단계 인증" +totp: "인증 앱" +totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력" moderator: "모더레이터" moderation: "모더레이션" nUsersMentioned: "{n}명이 언급함" +securityKeyAndPasskey: "보안 키 또는 패스 키" securityKey: "보안 키" -securityKeyName: "키 이름" -registerSecurityKey: "보안 키를 등록" lastUsed: "마지막 사용" +lastUsedAt: "마지막 사용: {t}" unregister: "등록 해제" passwordLessLogin: "비밀번호 없이 로그인" +passwordLessLoginDescription: "비밀번호를 사용하지 않고 보안 키 또는 패스 키 등으로만 로그인합니다." resetPassword: "비밀번호 재설정" newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다" reduceUiAnimation: "UI의 애니메이션을 줄이기" @@ -412,24 +420,15 @@ markAsReadAllTalkMessages: "모든 대화를 읽은 상태로 표시" help: "도움말" inputMessageHere: "여기에 메시지를 입력하세요" close: "닫기" -group: "그룹" -groups: "그룹" -createGroup: "그룹 만들기" -ownedGroups: "소유 그룹" -joinedGroups: "참여중인 그룹" invites: "초대" -groupName: "그룹명" members: "멤버" transfer: "양도" -messagingWithUser: "유저와 대화하기" -messagingWithGroup: "그룹끼리 대화하기" title: "제목" text: "텍스트" enable: "사용" next: "다음" retype: "다시 입력" noteOf: "{user}의 노트" -inviteToGroup: "그룹에 초대하기" quoteAttached: "인용함" quoteQuestion: "인용해서 작성하시겠습니까?" noMessagesYet: "아직 대화가 없습니다" @@ -451,20 +450,18 @@ passwordMatched: "일치합니다" passwordNotMatched: "일치하지 않습니다" signinWith: "{x}로 로그인" signinFailed: "로그인할 수 없습니다. 사용자명과 비밀번호를 확인하여 주십시오." -tapSecurityKey: "보안 키를 터치" or: "혹은" language: "언어" uiLanguage: "UI 표시 언어" -groupInvited: "그룹에 초대되었습니다" aboutX: "{x}에 대하여" emojiStyle: "이모지 스타일" native: "네이티브" disableDrawer: "드로어 메뉴를 사용하지 않기" -youHaveNoGroups: "그룹이 없습니다" -joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들어 보세요." +showNoteActionsOnlyHover: "노트 액션 버튼을 마우스를 올렸을 때에만 표시" noHistory: "기록이 없습니다" signinHistory: "로그인 기록" -disableAnimatedMfm: "움직임이 있는 MFM을 비활성화" +enableAdvancedMfm: "고급 MFM을 활성화" +enableAnimatedMfm: "움직임이 있는 MFM을 활성화" doing: "잠시만요" category: "카테고리" tags: "태그" @@ -541,7 +538,7 @@ updateRemoteUser: "리모트 유저 정보 갱신" deleteAllFiles: "모든 파일 삭제" deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?" removeAllFollowing: "모든 팔로잉 해제" -removeAllFollowingDescription: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 인스턴스가 더 이상 존재하지 않게 된 경우 등에 실행해 주세요." +removeAllFollowingDescription: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않게 된 경우 등에 실행해 주세요." userSuspended: "이 계정은 정지된 상태입니다." userSilenced: "이 계정은 사일런스된 상태입니다." yourAccountSuspendedTitle: "계정이 정지되었습니다" @@ -607,7 +604,7 @@ testEmail: "이메일 전송 테스트" wordMute: "단어 뮤트" regexpError: "정규 표현식 오류" regexpErrorDescription: "{tab}단어 뮤트 {line}행의 정규 표현식에 오류가 발생했습니다:" -instanceMute: "인스턴스 뮤트" +instanceMute: "서버 뮤트" userSaysSomething: "{name}님이 무언가를 말했습니다" makeActive: "활성화" display: "표시" @@ -638,15 +635,15 @@ abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다." reporter: "신고자" reporteeOrigin: "피신고자" reporterOrigin: "신고자" -forwardReport: "리모트 인스턴스에도 신고 내용 보내기" -forwardReportIsAnonymous: "리모트 인스턴스에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다." +forwardReport: "리모트 서버에도 신고 내용 보내기" +forwardReportIsAnonymous: "리모트 서버에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다." send: "전송" abuseMarkAsResolved: "해결됨으로 표시" openInNewTab: "새 탭에서 열기" openInSideView: "사이드뷰로 열기" defaultNavigationBehaviour: "기본 탐색 동작" editTheseSettingsMayBreakAccount: "이 설정을 변경하면 계정이 손상될 수 있습니다." -instanceTicker: "노트의 인스턴스 정보" +instanceTicker: "노트의 서버 정보" waitingFor: "{x}을(를) 기다리고 있습니다" random: "랜덤" system: "시스템" @@ -735,7 +732,7 @@ capacity: "용량" inUse: "사용중" editCode: "코드 수정" apply: "적용" -receiveAnnouncementFromInstance: "이 인스턴스의 알림을 이메일로 수신할게요" +receiveAnnouncementFromInstance: "이 서버의 알림을 이메일로 수신할게요" emailNotification: "메일 알림" publish: "게시" inChannelSearch: "채널에서 검색" @@ -763,7 +760,7 @@ active: "최근에 활동함" offline: "오프라인" notRecommended: "추천하지 않음" botProtection: "Bot 방어" -instanceBlocking: "인스턴스 차단" +instanceBlocking: "서버 차단" selectAccount: "계정 선택" switchAccount: "계정 바꾸기" enabled: "활성화" @@ -783,6 +780,7 @@ popularPosts: "인기 포스트" shareWithNote: "노트로 공유" ads: "광고" expiration: "기한" +startingperiod: "시작 기간" memo: "메모" priority: "우선순위" high: "높음" @@ -815,6 +813,7 @@ lastCommunication: "마지막 통신" resolved: "해결됨" unresolved: "해결되지 않음" breakFollow: "팔로워 해제" +breakFollowConfirm: "팔로우를 해제하시겠습니까?" itsOn: "켜짐" itsOff: "꺼짐" emailRequiredForSignup: "가입할 때 이메일 주소 입력을 필수로 하기" @@ -834,8 +833,6 @@ deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. incorrectPassword: "비밀번호가 올바르지 않습니다." voteConfirm: "\"{choice}\"에 투표하시겠습니까?" hide: "숨기기" -leaveGroup: "그룹 나가기" -leaveGroupConfirm: "\"{name}\"에서 나갈까요?" useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시" welcomeBackWithName: "환영합니다, {name}님" clickToFinishEmailVerification: "[{ok}]를 눌러 이메일 인증을 완료하세요." @@ -847,20 +844,24 @@ themeColor: "테마 컬러" size: "크기" numberOfColumn: "한 줄에 보일 리액션의 수" searchByGoogle: "검색" -instanceDefaultLightTheme: "인스턴스 기본 라이트 테마" -instanceDefaultDarkTheme: "인스턴스 기본 다크 테마" +instanceDefaultLightTheme: "서버 기본 라이트 테마" +instanceDefaultDarkTheme: "서버 기본 다크 테마" instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요." mutePeriod: "뮤트할 기간" +period: "투표 기한" indefinitely: "무기한" tenMinutes: "10분" oneHour: "1시간" oneDay: "1일" oneWeek: "일주일" +oneMonth: "1개월" reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다." failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다" rateLimitExceeded: "요청 제한 횟수를 초과하였습니다" cropImage: "이미지 자르기" cropImageAsk: "이미지를 자르시겠습니까?" +cropYes: "잘라내기" +cropNo: "그대로 사용" file: "파일" recentNHours: "최근 {n}시간" recentNDays: "최근 {n}일" @@ -897,7 +898,7 @@ cannotUploadBecauseInappropriate: "이 파일은 부적절한 내용을 포함 cannotUploadBecauseNoFreeSpace: "드라이브 용량이 부족하여 업로드할 수 없습니다." beta: "베타" enableAutoSensitive: "자동 NSFW 탐지" -enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 인스턴스 정책에 따라 자동으로 설정될 수 있습니다." +enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 서버 정책에 따라 자동으로 설정될 수 있습니다." activeEmailValidationDescription: "유저가 입력한 메일 주소가 일회용 메일인지, 실제로 통신할 수 있는 지 엄격하게 검사합니다. 해제할 경우 이메일 형식에 대해서만 검사합니다." navbar: "내비게이션 바" shuffle: "셔플" @@ -907,7 +908,7 @@ pushNotification: "푸시 알림" subscribePushNotification: "푸시 알림 켜기" unsubscribePushNotification: "푸시 알림 끄기" pushNotificationAlreadySubscribed: "푸시 알림이 이미 켜져 있습니다" -pushNotificationNotSupported: "브라우저나 인스턴스에서 푸시 알림이 지원되지 않습니다" +pushNotificationNotSupported: "브라우저나 서버에서 푸시 알림이 지원되지 않습니다" sendPushNotificationReadMessage: "푸시 알림이나 메시지를 읽은 뒤 푸시 알림을 삭제" sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」이라는 알림이 잠깐 표시됩니다. 기기의 전력 소비량이 증가할 수 있습니다." windowMaximize: "최대화" @@ -939,6 +940,21 @@ cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시 preset: "프리셋" selectFromPresets: "프리셋에서 선택" achievements: "도전 과제" +gotInvalidResponseError: "서버의 응답이 올바르지 않습니다" +gotInvalidResponseErrorDescription: " 서버가 다운되었거나 점검중일 가능성이 있습니다. 잠시후에 다시 시도해 주십시오." +thisPostMayBeAnnoying: "이 게시물은 다른 유저에게 피해를 줄 가능성이 있습니다." +thisPostMayBeAnnoyingHome: "홈에 게시" +thisPostMayBeAnnoyingCancel: "그만두기" +thisPostMayBeAnnoyingIgnore: "이대로 게시" +collapseRenotes: "이미 본 리노트를 간략화하기" +internalServerError: "내부 서버 오류" +internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다." +copyErrorInfo: "오류 정보 복사" +joinThisServer: "이 서버에 가입" +exploreOtherServers: "다른 서버 둘러보기" +letsLookAtTimeline: "타임라인 구경하기" +disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다." +invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다." _achievements: earnedAt: "달성 일시" _types: @@ -1129,7 +1145,7 @@ _achievements: description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었습니다" _viewInstanceChart: title: "애널리스트" - description: "인스턴스의 차트를 열었습니다" + description: "서버의 차트를 열었습니다" _outputHelloWorldOnScratchpad: title: "Hello, world!" description: "스크래치패드에서 hello world를 출력했습니다" @@ -1166,7 +1182,7 @@ _achievements: _loggedInOnNewYearsDay: title: "새해 복 많이 받으세요" description: "새해 첫 날에 로그인했습니다" - flavor: "올해에도 저희 인스턴스에 관심을 가져 주셔서 감사합니다" + flavor: "올해에도 저희 서버에 관심을 가져 주셔서 감사합니다" _cookieClicked: title: "쿠키를 클릭하는 게임" description: "쿠키를 클릭했습니다" @@ -1181,7 +1197,7 @@ _role: name: "역할 이름" description: "역할 설명" permission: "역할 권한" - descriptionOfPermission: "모더레이터는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n관리자는 인스턴스의 모든 설정을 변경할 수 있습니다." + descriptionOfPermission: "모더레이터는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n관리자는 서버의 모든 설정을 변경할 수 있습니다." assignTarget: "할당 대상" descriptionOfAssignTarget: "수동을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n조건부를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다." manual: "수동" @@ -1195,6 +1211,9 @@ _role: baseRole: "기본 역할" useBaseValue: "기본값 사용" chooseRoleToAssign: "할당할 역할 선택" + iconUrl: "아이콘 URL" + asBadge: "뱃지로 표시" + descriptionOfAsBadge: "활성화하면 유저명 옆에 역할의 아이콘이 표시됩니다." canEditMembersByModerator: "모더레이터의 역할 수정 허용" descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다." priority: "우선순위" @@ -1206,7 +1225,7 @@ _role: gtlAvailable: "글로벌 타임라인 보이기" ltlAvailable: "로컬 타임라인 보이기" canPublicNote: "공개 노트 허용" - canInvite: "인스턴스 초대 코드 발행" + canInvite: "서버 초대 코드 발행" canManageCustomEmojis: "커스텀 이모지 관리" driveCapacity: "드라이브 용량" pinMax: "고정할 수 있는 노트 수" @@ -1217,7 +1236,7 @@ _role: noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수" userListMax: "생성할 수 있는 유저 리스트 수" userEachUserListsMax: "유저 리스트당 최대 사용자 수" - rateLimitFactor: "속도 제한" + rateLimitFactor: "요청 빈도 제한" descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다." canHideAds: "광고 숨기기" _condition: @@ -1268,7 +1287,7 @@ _ad: _forgotPassword: enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다." ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오." - contactAdmin: "이 인스턴스에서는 메일 기능이 지원되지 않습니다. 비밀번호를 재설정하려면 관리자에게 문의해 주십시오." + contactAdmin: "이 서버에서는 메일 기능이 지원되지 않습니다. 비밀번호를 재설정하려면 관리자에게 문의해 주십시오." _gallery: my: "내 갤러리" liked: "좋아요 한 갤러리" @@ -1320,72 +1339,6 @@ _nsfw: respect: "열람주의로 설정된 미디어 숨기기" ignore: "열람 주의 미디어 항상 표시" force: "미디어 항상 숨기기" -_mfm: - cheatSheet: "MFM 도움말" - intro: "MFM는 Misskey의 다양한 곳에서 사용할 수 있는 전용 마크업 언어입니다. 여기에서는 MFM에서 사용할 수 있는 구문을 확인할 수 있습니다." - dummy: "Misskey로 연합우주의 세계가 펼쳐집니다" - mention: "멘션" - mentionDescription: "골뱅이표(@) 뒤에 사용자명을 넣어 특정 유저를 나타낼 수 있습니다." - hashtag: "해시태그" - hashtagDescription: "샵 또는 우물정자(#)를 앞에 붙여서 해시태그를 나타낼 수 있습니다." - url: "URL" - urlDescription: "URL을 나타낼 수 있습니다." - link: "링크" - linkDescription: "문장의 특정 범위를 URL로 표시합니다." - bold: "굵음/볼드체" - boldDescription: "문자를 굵게 강조합니다." - small: "눈에 띄지 않음" - smallDescription: "내용을 작고 연하게 보이게 합니다." - center: "가운데 정렬" - centerDescription: "내용을 가운데 정렬로 보이게 합니다." - inlineCode: "코드(인라인)" - inlineCodeDescription: "여러 행의 코드를 문법 강조를 적용하여 인라인으로 표시합니다." - blockCode: "코드(블록)" - blockCodeDescription: "여러 행의 코드를 문법 강조를 적용하여 블록으로 표시합니다." - inlineMath: "수식(인라인)" - inlineMathDescription: "수식(KaTeX)를 인라인으로 보이게 합니다." - blockMath: "수식(블록)" - blockMathDescription: "여러 줄의 수식(KaTeX)를 블록으로 보이게 합니다." - quote: "인용" - quoteDescription: "내용을 인용문으로 표시합니다." - emoji: "커스텀 이모지" - emojiDescription: "커스텀 이모지의 이름을 쌍점(:)으로 감싸서 커스텀 이모지를 사용합니다." - search: "검색" - searchDescription: "주어진 키워드가 입력된 검색창을 보이게 합니다." - flip: "플립" - flipDescription: "내용을 상하 또는 좌우로 반전시킵니다." - jelly: "애니메이션 (젤리)" - jellyDescription: "젤리처럼 탱글탱글한 느낌의 효과를 줍니다." - tada: "애니메이션 (짠!)" - tadaDescription: "짠! 하는 느낌의 효과를 줍니다." - jump: "애니메이션(점프)" - jumpDescription: "펄쩍 뛸 듯한 느낌의 효과를 줍니다." - bounce: "애니메이션 (바운스)" - bounceDescription: "통통 튀는 느낌의 효과를 줍니다." - shake: "애니메이션 (부들부들)" - shakeDescription: "부들부들 떠는 느낌의 효과를 줍니다." - twitch: "애니메이션 (경련)" - twitchDescription: "격하게 흔들리는 느낌의 효과를 줍니다." - spin: "애니메이션 (회전)" - spinDescription: "회전 효과를 줍니다." - x2: "크게" - x2Description: "내용을 크게 표시합니다." - x3: "더 크게" - x3Description: "내용을 더 크게 표시합니다." - x4: "매우 크게" - x4Description: "내용을 매우 크게 표시합니다." - blur: "흐림" - blurDescription: "내용이 흐리게 보입니다. 마우스를 위에 올려두면 내용이 보입니다." - font: "폰트" - fontDescription: "내용의 글꼴을 지정할 수 있습니다." - rainbow: "무지개" - rainbowDescription: "내용을 무지개로 표시합니다." - sparkle: "반짝반짝" - sparkleDescription: "반짝이는 파티클 효과를 추가합니다." - rotate: "회전" - rotateDescription: "지정한 각도로 회전시킵니다." - plain: "평문" - plainDescription: "안에 있는 MFM 구문을 모두 무시하고 평문으로 표시합니다." _instanceTicker: none: "보이지 않음" remote: "리모트 유저에게만 보이기" @@ -1419,10 +1372,10 @@ _wordMute: hard: "보다 높은 수준" mutedNotes: "뮤트된 노트" _instanceMute: - instanceMuteDescription: "뮤트한 인스턴스에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다." + instanceMuteDescription: "뮤트한 서버에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다." instanceMuteDescription2: "한 줄에 하나씩 입력해 주세요" - title: "지정한 인스턴스의 노트를 숨깁니다." - heading: "뮤트할 인스턴스" + title: "지정한 서버의 노트를 숨깁니다." + heading: "뮤트할 서버" _theme: explore: "테마 찾아보기" install: "테마 설치" @@ -1515,6 +1468,7 @@ _ago: weeksAgo: "{n}주 전" monthsAgo: "{n}개월 전" yearsAgo: "{n}년 전" + invalid: "없음" _time: second: "초" minute: "분" @@ -1534,7 +1488,7 @@ _tutorial: step4_1: "노트 작성을 끝내셨나요?" step4_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다." step5_1: "이제, 다른 사람을 팔로우하여 타임라인을 활기차게 만들어보도록 합시다." - step5_2: "{featured}에서 이 인스턴스의 인기 노트를 보실 수 있습니다. {explore}에서는 인기 사용자를 찾을 수 있구요. 마음에 드는 사람을 골라 팔로우해 보세요!" + step5_2: "{featured}에서 이 서버의 인기 노트를 보실 수 있습니다. {explore}에서는 인기 사용자를 찾을 수 있구요. 마음에 드는 사람을 골라 팔로우해 보세요!" step5_3: "다른 유저를 팔로우하려면 해당 유저의 아이콘을 클릭하여 프로필 페이지를 띄운 후, 팔로우 버튼을 눌러 주세요." step5_4: "사용자에 따라 팔로우가 승인될 때까지 시간이 걸릴 수 있습니다." step6_1: "타임라인에 다른 사용자의 노트가 나타난다면 성공입니다." @@ -1548,14 +1502,29 @@ _tutorial: step8_3: "알림 설정은 나중에도 변경할 수 있습니다." _2fa: alreadyRegistered: "이미 설정이 완료되었습니다." - registerDevice: "디바이스 등록" - registerKey: "키를 등록" + registerTOTP: "인증 앱 설정 시작" + passwordToTOTP: "비밀번호를 입력하세요." step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다." step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다." + step2Click: "QR 코드를 클릭하면 기기에 설치된 인증 앱에 등록할 수 있습니다." step2Url: "데스크톱 앱에서는 다음 URL을 입력하세요:" + step3Title: "인증 코드 입력" step3: "앱에 표시된 토큰을 입력하시면 완료됩니다." step4: "다음 로그인부터는 토큰을 입력해야 합니다." + securityKeyNotSupported: "이 브라우저는 보안 키를 지원하지 않습니다." + registerTOTPBeforeKey: "보안 키 또는 패스키를 등록하려면 인증 앱을 등록하십시오." securityKeyInfo: "FIDO2를 지원하는 하드웨어 보안 키 혹은 디바이스의 지문인식이나 화면잠금 PIN을 이용해서 로그인하도록 설정할 수 있습니다." + chromePasskeyNotSupported: "현재 Chrome의 패스키는 지원되지 않습니다." + registerSecurityKey: "보안 키 또는 패스키 등록" + securityKeyName: "키 이름 입력" + tapSecurityKey: "브라우저의 지시에 따라 보안 키 또는 패스키를 등록하여 주십시오" + removeKey: "보안 키를 삭제" + removeKeyConfirm: "{name} 을(를) 삭제하시겠습니까?" + whyTOTPOnlyRenew: "보안 키가 등록되어 있는 경우 인증 앱을 해제할 수 없습니다." + renewTOTP: "인증 앱 재설정" + renewTOTPConfirm: "기존에 등록되어 있던 인증 키는 사용하지 못하게 됩니다." + renewTOTPOk: "재설정" + renewTOTPCancel: "취소" _permissions: "read:account": "계정의 정보를 봅니다" "write:account": "계정의 정보를 변경합니다" @@ -1590,18 +1559,20 @@ _permissions: "read:gallery-likes": "갤러리의 좋아요를 확인합니다" "write:gallery-likes": "갤러리에 좋아요를 추가하거나 취소합니다" _auth: + shareAccessTitle: "어플리케이션의 접근 허가" shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?" shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용하시겠습니까?" + permission: "{name}에서 다음 권한을 요청하였습니다" permissionAsk: "이 앱은 다음의 권한을 요청합니다" pleaseGoBack: "앱으로 돌아가서 시도해 주세요" callback: "앱으로 돌아갑니다" denied: "접근이 거부되었습니다" + pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오." _antennaSources: all: "모든 노트" homeTimeline: "팔로우중인 유저의 노트" users: "지정한 한 명 혹은 여러 명의 유저의 노트" userList: "지정한 리스트에 속한 유저의 노트" - userGroup: "지정한 그룹에 속한 유저의 노트" _weekday: sunday: "일요일" monday: "월요일" @@ -1612,7 +1583,7 @@ _weekday: saturday: "토요일" _widgets: profile: "프로필" - instanceInfo: "인스턴스 정보" + instanceInfo: "서버 정보" memo: "스티커 메모" notifications: "알림" timeline: "타임라인" @@ -1626,7 +1597,7 @@ _widgets: digitalClock: "디지털 시계" unixClock: "UNIX 시계" federation: "연합" - instanceCloud: "인스턴스 구름" + instanceCloud: "서버 구름" postForm: "글 입력란" slideshow: "슬라이드 쇼" button: "버튼" @@ -1676,8 +1647,8 @@ _visibility: followersDescription: "팔로워에게만 공개" specified: "다이렉트" specifiedDescription: "지정한 유저에게만 공개" - localOnly: "로컬에만" - localOnlyDescription: "리모트 유저에게 보이지 않기" + disableFederation: "연합에 보내지 않기" + disableFederationDescription: "다른 서버로 보내지 않습니다" _postForm: replyPlaceholder: "이 노트에 답글..." quotePlaceholder: "이 노트를 인용..." @@ -1815,12 +1786,9 @@ _notification: youGotReply: "{name}님이 답글함" youGotQuote: "{name}님이 인용함" youRenoted: "{name}님이 Renote" - youGotMessagingMessageFromUser: "{name} 님이 보낸 채팅이 있어요" - youGotMessagingMessageFromGroup: "{name}에서 보낸 채팅이 있어요" youWereFollowed: "새로운 팔로워가 있습니다" youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다" yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다" - youWereInvitedToGroup: "그룹에 초대되었습니다" pollEnded: "투표 결과가 발표되었습니다" unreadAntennaNote: "안테나 {name}" emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다" @@ -1836,7 +1804,7 @@ _notification: pollEnded: "투표가 종료됨" receiveFollowRequest: "팔로우 요청을 받았을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때" - groupInvited: "그룹에 초대되었을 때" + achievementEarned: "도전 과제 획득" app: "연동된 앱을 통한 알림" _actions: followBack: "팔로우" @@ -1869,3 +1837,6 @@ _deck: channel: "채널" mentions: "받은 멘션" direct: "다이렉트" +_dialog: + charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}" + charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index befb2eb369..ce596d038f 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -99,19 +99,108 @@ followRequestPending: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍ enterEmoji: "ປ້ອນອີໂມຈິ" renote: "Renote" unrenote: "ເລີກ Renote" +renoted: "ເກັບບັນທຶກໄວ້" +quote: "ລວມຂໍ້ຄວາມອ້າງອີງ" +pinnedNote: "ບັນທຶກທີ່ປັກໝຸດໄວ້" pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" +you: "ເຈົ້າ" +clickToShow: "ກົດເພື່ອສະແດງໃຫ້ເຫັນ" +sensitive: "NSFW" +add: "ເພີ່ມ" +reaction: "ປະຕິກິລິຍາ" +reactions: "ປະຕິກິລິຍາ" +mute: "ປີດສຽງ" +unmute: "ເປີດສຽງ" +block: "ບ໋ອກ" +unblock: "ຍົກເລີກກາຮົບລັອກ" +suspend: "ລະງັບ" +unsuspend: "ເຊົາ​ລະ​ງັບ" +selectList: "ເລືອກບັນຊີລາຍການ" +selectWidget: "ເລືອກວິກເຈັດ" +editWidgets: "ແກ້ໄຂ Widget" +editWidgetsExit: "ສຳເລັດແລ້ວ" +customEmojis: "ອີໂມຈິແບບກຳນົດເອງ" +emoji: "ອີໂມຈິ" +emojis: "ອີໂມຈິ" +emojiName: "ຊື່ Emoji" +emojiUrl: "URL ອີໂມຈິ" +addEmoji: "ຕື່ມອີໂມຈິ" +flagAsBot: "ໝາຍບັນຊີນີ້ເປັນບັອດ" +flagAsCat: "ໝາຍບັນຊີນີ້ເປັນແມວ" +flagAsCatDescription: "ເປີດໃຊ້ຕົວເລືອກນີ້ເພື່ອໝາຍບັນຊີນີ້ເປັນແມວ" +flagShowTimelineReplies: "ສະແດງການຕອບກັບໃນທາມລາຍ" +flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບຂອງຜູ້ໃຊ້ຕໍ່ກັບບັນທຶກຂອງຜູ້ໃຊ້ອື່ນໃນທາມລາຍຖ້າເປີດໃຊ້ງານ" +autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່" addAccount: "ເພີ່ມບັນຊີ" loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ" general: "ທົ່ວໄປ" wallpaper: "ພາບພື້ນຫລັງ" setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ" +searchWith: "ຊອກຫາ: {q}" +proxyAccount: "ບັນຊີພຣັອກຊີ" +host: "ໂຮດສ" +selectUser: "ເລືອກຜູ້ໃຊ້" +recipient: "ເຖິງ" +annotation: "ຄຳເຫັນ" +federation: "ສະຫະພັນ" instances: "ອີນສະແຕນ" +registeredAt: "ລົງທະບຽນຢູ່" +storageUsage: "ບ່ອນ​ຈັດ​ເກັບ​ຂໍ້​ມູນທີ່ໃຊ້" +charts: "ອັນດັບເພງ" +perHour: "ຕໍ່ຊົ່ວໂມງ" +perDay: "ຕໍ່​ມື້" +stopActivityDelivery: "ຢຸດເຊົາການສົ່ງກິດຈະກໍາ" +blockThisInstance: "ຂັດຂວາງຕົວຢ່າງນີ້" +operations: "ການດຳເນີນງານ" +software: "ຊອບແວ" +version: "ສະບັບ" +metadata: "Metadata" +monitor: "ຈໍພາບ" +cpuAndMemory: "CPU ແລະ ຫນ່ວຍຄວາມຈໍາ" +network: "ເຄືອຂ່າຍ" +disk: "ດິສກ໌" +instanceInfo: "ອີນສະແຕນ" statistics: "ສະຖິຕິ" clearQueue: "ລ້າງຄິວ" clearCachedFiles: "ລຶບລ້າງແຄສ" editProfile: "ແກ້ໄຂໂປຣໄຟລ໌" +done: "ສຳເລັດ" +processing: "ກຳລັງປະມວນຜົນ" +preview: "ສະແດງເປັນຕົວຢ່າງ" +default: "ຄ່າເລີ່ມຕົ້ນ" +blocked: "ບລັອກແລ້ວ " +all: "ທັງໝົດ" +subscribing: "ສະໝັກສະມາຊິກແລັວ" +publishing: "ການ​ພິມ​ເຜີຍ​ແຜ່" +notResponding: "ບໍ່ຕອບສະໜອງ" +instanceFollowing: "ກຳລັງຕິດຕາມສຸດຕົວຢ່າງ" +instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ" +instanceUsers: "ຜູ້​ຊົມ​ໃຊ້​ຂອງ​ຕົວ​ຢ່າງ​ນີ້​" +changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ" +featured: "ໄຮໄລທ໌" +announcements: "ປະກາດ" remove: "ລຶບ" +messaging: "ແຊ໋ດ" +tos: "ເງື່ອນໄຂການໃຫ້ບໍລິການ" +start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ" +home: "ໜ້າຫຼັກ" +images: "ຮູບພາບ" +birthday: "ວັນເກີດ" +registeredDate: "ວັນທີ່ເປັນສະມາຊິກ" +location: "ທີ່ຕັ້ງ" +theme: "ແທ໋ມ" +light: "ສະຫວ່າງ" +dark: "ມືດ" +lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ" +darkThemes: "ຮູບແບບສີສັນມືດ" +fileName: "ຊື່ໄຟລ໌" +selectFile: "ເລືອກໄຟລ໌" +selectFiles: "ເລືອກໄຟລ໌" +nsfw: "NSFW" +accept: "ອະນຸຍາດ" +pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້" userList: "ລາຍການ" +smtpHost: "ໂຮດສ" smtpUser: "ຊື່ຜູ້ໃຊ້" smtpPass: "ລະຫັດຜ່ານ" clearCache: "ລຶບລ້າງແຄສ" @@ -121,9 +210,6 @@ file: "ໄຟລ໌" _email: _follow: title: "ໄດ້ຕິດຕາມທ່ານ" -_mfm: - mention: "ໄດ້ກ່າວມາ" - search: "ຄົ້ນຫາ" _theme: keys: mention: "ໄດ້ກ່າວມາ" @@ -131,25 +217,44 @@ _theme: _sfx: note: "ບັນທຶກ" notification: "ການແຈ້ງເຕືອນ" + chat: "ແຊ໋ດ" +_2fa: + renewTOTPCancel: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້" _widgets: profile: "ໂພຼຟາຍ" + instanceInfo: "ອີນສະແຕນ" notifications: "ການແຈ້ງເຕືອນ" timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" + federation: "ສະຫະພັນ" + _userList: + chooseList: "ເລືອກບັນຊີລາຍການ" _cw: show: "ໂຫຼດເພີ່ມເຕີມ" _visibility: + home: "ໜ້າຫຼັກ" followers: "ຜູ້ຕິດຕາມ" _profile: username: "ຊື່ຜູ້ໃຊ້" _exportOrImport: followingList: "ກຳລັງຕິດຕາມ" + muteList: "ປີດສຽງ" + blockingList: "ບ໋ອກ" userLists: "ລາຍການ" +_charts: + federation: "ສະຫະພັນ" +_timelines: + home: "ໜ້າຫຼັກ" +_pages: + blocks: + image: "ຮູບພາບ" _notification: youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ" _types: follow: "ກຳລັງຕິດຕາມ" mention: "ໄດ້ກ່າວມາ" renote: "Renote" + quote: "ລວມຂໍ້ຄວາມອ້າງອີງ" + reaction: "ປະຕິກິລິຍາ" _actions: reply: "ຕອບ​ໄປ​ທີ" renote: "Renote" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index e99d49710d..3d33b5227e 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -376,13 +376,10 @@ about: "Over" aboutMisskey: "Over Misskey" administrator: "Beheerder" token: "Token" -twoStepAuthentication: "Tweestapsverificatie" moderator: "Moderator" moderation: "Moderatie" nUsersMentioned: "Vermeld door {n} gebruikers" securityKey: "Beveiligingssleutel" -securityKeyName: "Sleutelnaam" -registerSecurityKey: "Zekerheids-Sleutel registreren" lastUsed: "Laatst gebruikt" unregister: "Uitschrijven" passwordLessLogin: "Inloggen zonder wachtwoord" @@ -399,8 +396,6 @@ markAsReadAllTalkMessages: "Markeer alle berichten als gelezen" help: "Help" inputMessageHere: "Voer hier je bericht in" close: "Sluiten" -group: "Groep" -groups: "Groepen" invites: "Uitnodigen" invitations: "Uitnodigen" sound: "Geluid" @@ -427,11 +422,6 @@ loggedInAsBot: "Momenteel als bot ingelogd" _email: _follow: title: "volgde jou" -_mfm: - mention: "Vermelding" - quote: "Quote" - emoji: "Maatwerk emoji" - search: "Zoeken" _theme: keys: mention: "Vermelding" @@ -440,6 +430,8 @@ _sfx: note: "Notities" notification: "Meldingen" chat: "Chat" +_2fa: + renewTOTPCancel: "Nee, bedankt" _widgets: profile: "Profiel" instanceInfo: "Serverinformatie" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index d8c7739f6c..1dc818d459 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -385,13 +385,10 @@ about: "Informacje" aboutMisskey: "O Misskey" administrator: "Admin" token: "Token" -twoStepAuthentication: "Uwierzytelnianie dwuskładnikowe" moderator: "Moderator" moderation: "Moderacja" nUsersMentioned: "{n} wspomnianych użytkowników" securityKey: "Klucz bezpieczeństwa" -securityKeyName: "Nazwa klucza" -registerSecurityKey: "Zarejestruj klucz bezpieczeństwa" lastUsed: "Ostatnio używane" unregister: "Cofnij rejestrację" passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła" @@ -409,24 +406,15 @@ markAsReadAllTalkMessages: "Oznacz wszystkie wiadomości jako przeczytane" help: "Pomoc" inputMessageHere: "Wprowadź wiadomość tutaj" close: "Zamknij" -group: "Grupy" -groups: "Grupy" -createGroup: "Utwórz grupę" -ownedGroups: "Posiadane grupy" -joinedGroups: "Członkostwa w grupach" invites: "Zaproś" -groupName: "Nazwa grupy" members: "Członkowie" transfer: "Transfer" -messagingWithUser: "Rozmowy z innym użytkownikiem" -messagingWithGroup: "Rozmowy wewnątrz grupy" title: "Tytuł" text: "Tekst" enable: "Włącz" next: "Dalej" retype: "Wprowadź ponownie" noteOf: "Wpisy {user}" -inviteToGroup: "Zaproś do grupy" quoteAttached: "Zacytowano" quoteQuestion: "Czy na pewno chcesz umieścić cytat?" noMessagesYet: "Nie napisano jeszcze wiadomości" @@ -448,20 +436,15 @@ passwordMatched: "Pasuje" passwordNotMatched: "Hasła nie pasują do siebie" signinWith: "Zaloguj się z {x}" signinFailed: "Nie udało się zalogować. Wprowadzona nazwa użytkownika lub hasło są nieprawidłowe." -tapSecurityKey: "Wybierz swój klucz bezpieczeństwa" or: "Lub" language: "Język" uiLanguage: "Język wyświetlania UI" -groupInvited: "Zaproszony(-a) do grupy" aboutX: "O {x}" emojiStyle: "Styl emoji" native: "Natywny" disableDrawer: "Nie używaj menu w stylu szuflady" -youHaveNoGroups: "Nie masz żadnych grup" -joinOrCreateGroup: "Uzyskaj zaproszenie do dołączenia do grupy lub utwórz własną grupę." noHistory: "Brak historii" signinHistory: "Historia logowania" -disableAnimatedMfm: "Wyłącz MFM z animacją" doing: "Przetwarzanie..." category: "Kategoria" tags: "Tagi" @@ -817,8 +800,6 @@ deleteAccountConfirm: "Spowoduje to nieodwracalne usunięcie Twojego konta. Kont incorrectPassword: "Nieprawidłowe hasło." voteConfirm: "Potwierdzić swój głos na \"{choice}\"?" hide: "Ukryj" -leaveGroup: "Opuść grupę" -leaveGroupConfirm: "Czy na pewno chcesz opuścić \"{name}\"?" useDrawerReactionPickerForMobile: "Wyświetlaj wybornik reakcji jako szufladę na urządzeniach mobilnych" welcomeBackWithName: "Witaj z powrotem, {name}" clickToFinishEmailVerification: "Kliknij [{ok}], aby zakończyć weryfikację e-mail." @@ -829,6 +810,7 @@ auto: "Automatycznie" size: "Rozmiar" numberOfColumn: "Liczba kolumn" searchByGoogle: "Szukaj" +period: "Ankieta kończy się" indefinitely: "Nigdy" tenMinutes: "10 minut" oneHour: "1 godzina" @@ -958,68 +940,6 @@ _nsfw: respect: "Ukrywaj media NSFW" ignore: "Nie ukrywaj mediów NSFW" force: "Ukrywaj wszystkie media" -_mfm: - cheatSheet: "Ściąga MFM" - intro: "MFM to język składniowy wyjątkowy dla Misskey, który może być użyty w wielu miejscach. Tu znajdziesz listę wszystkich możliwych elementów składni MFM." - dummy: "Misskey rozszerza świat Fediwersum" - mention: "Wspomnij" - mentionDescription: "Używając znaku @ i nazwy użytkownika, możesz określić danego użytkownika." - hashtag: "Hashtag" - hashtagDescription: "Używając kratki i tekstu, możesz określić hashtag." - url: "Adres URL" - urlDescription: "Adresy URL mogą być wyświetlane" - link: "Odnośnik" - linkDescription: "Określone części tekstu mogą być wyświetlane jako adres URL." - bold: "Pogrubienie" - boldDescription: "Wyróżnia litery pogrubiając je." - small: "Małe" - smallDescription: "Wyświetla treść jako małą i cienką." - center: "Wyśrodkowanie" - centerDescription: "Wyśrodkowuje zawartość." - inlineCode: "Kod (w wierszu)" - blockCode: "Kod (blok)" - blockCodeDescription: "Wyświetla kod z podświetlaną składnią składający się z wielu linii." - blockMath: "Matematyka (Blok)" - quote: "Cytuj" - quoteDescription: "Wyświetla treść jako cytat." - emoji: "Niestandardowe emoji" - emojiDescription: "Otaczając nazwę niestandardowego emoji dwukropkami, możesz użyć niestandardowego emoji." - search: "Szukaj" - searchDescription: "Wyświetla pole wyszukiwania z wcześniej wpisanym tekstem." - flip: "Odwróć" - flipDescription: "Przerzuca treść poziomo lub pionowo." - jelly: "Animacja (Galaretka)" - jellyDescription: "Nadaje treści galaretowatą animację." - tada: "Animation (Tada)" - tadaDescription: "Nadaje treści animację podobną do \"Tada!\"." - jump: "Animacja (Skok)" - jumpDescription: "Nadaje treści animację skakania." - bounce: "Animacja (Odbijanie)" - bounceDescription: "Nadaje treści animację odbijania się." - shake: "Animacja (Wstrząsanie)" - shakeDescription: "Nadaje treści animację wstrząsania." - twitch: "Animacja (Drganie)" - twitchDescription: "Nadaje treści mocno drgającą animację." - spin: "Animacja (Obrót)" - spinDescription: "Nadaje treści animację obracania." - x2: "Duże" - x2Description: "Czyni treść większą." - x3: "Bardzo duże" - x3Description: "Czyni treść jeszcze większą." - x4: "Ogromne" - x4Description: "Czyni treść jeszcze większą niż jeszcze większa." - blur: "Rozmycie" - blurDescription: "Rozmywa treść. Zostanie wyraźnie wyświetlona po najechaniu." - font: "Czcionka" - fontDescription: "Wybiera czcionkę do wyświetlania treści." - rainbow: "Tęcza" - rainbowDescription: "Sprawia, że zawartość pojawia się w kolorach tęczy." - sparkle: "Blask" - sparkleDescription: "Nadaje zawartości efekt lśniącego brokatu." - rotate: "Obróć" - rotateDescription: "Obraca zawartość o określony kąt." - plain: "Zwyczajny" - plainDescription: "Wyłącza efekty wszystkich MFM zawartych w tym efekcie MFM." _instanceTicker: none: "Nigdy nie pokazuj" remote: "Pokaż dla zdalnych użytkowników" @@ -1142,6 +1062,7 @@ _ago: weeksAgo: "{n} tyg. temu" monthsAgo: "{n} mies. temu" yearsAgo: "{n} lat temu" + invalid: "Nie ma tu niczego" _time: second: "sekunda" minute: "minuta" @@ -1175,12 +1096,12 @@ _tutorial: step8_3: "Ustawienia powiadomień można zmienić później." _2fa: alreadyRegistered: "Zarejestrowałeś już urządzenie do uwierzytelniania dwuskładnikowego." - registerDevice: "Zarejestruj nowe urządzenie" - registerKey: "Zarejestruj klucz bezpieczeństwa" step1: "Najpierw, zainstaluj aplikację uwierzytelniającą (taką jak {a} lub {b}) na swoim urządzeniu." step2: "Następnie, zeskanuje kod QR z ekranu." step3: "Wprowadź token podany w aplikacji, aby ukończyć konfigurację." step4: "Od teraz, przy każdej próbie logowania otrzymasz prośbę o token logowania." + removeKeyConfirm: "Usunąć kopię zapasową {name}?" + renewTOTPCancel: "Nie teraz" _permissions: "read:account": "Wyświetl informacje o swoim koncie" "write:account": "Edytuj swoje informacje o koncie" @@ -1391,12 +1312,9 @@ _notification: youGotReply: "{name} odpowiedział(a) Tobie" youGotQuote: "{name} zacytował(a) Ciebie" youRenoted: "{name} udostępnił(a) Twój wpis" - youGotMessagingMessageFromUser: "{name} wysłał(a) Ci wiadomość" - youGotMessagingMessageFromGroup: "Została wysłana wiadomość do grupy {name}" youWereFollowed: "Zaobserwował(a) Cię" youReceivedFollowRequest: "Otrzymałeś(-aś) prośbę o możliwość obserwacji" yourFollowRequestAccepted: "Twoja prośba o możliwość obserwacji została przyjęta" - youWereInvitedToGroup: "Zaproszony(-a) do grupy" pollEnded: "Wyniki ankiety stały się dostępne" unreadAntennaNote: "Antena {name}" emptyPushNotificationMessage: "Powiadomienia push zostały zaktualizowane" @@ -1410,7 +1328,6 @@ _notification: reaction: "Reakcja" receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji" followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji" - groupInvited: "Zaproszono do grup" app: "Powiadomienia z aplikacji" _actions: followBack: "zaobserwował cię z powrotem" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 8eac5fee64..40b4aee7e6 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -382,12 +382,9 @@ about: "Informações" aboutMisskey: "Sobre Misskey" administrator: "Administrador" token: "Símbolo" -twoStepAuthentication: "Verificação em duas etapas" moderator: "Moderador" nUsersMentioned: "Postado por {n} pessoas" securityKey: "Chave de segurança" -securityKeyName: "Nome chave" -registerSecurityKey: "Registre a chave de segurança" lastUsed: "Último uso" unregister: "Cancelar registro" passwordLessLogin: "Entrar sem senha" @@ -405,10 +402,6 @@ markAsReadAllTalkMessages: "Marcar todas as conversas como lidas" help: "Ajuda" inputMessageHere: "Escrever mensagem aqui" close: "Fechar" -group: "Grupos" -groups: "Grupos" -createGroup: "Criar grupo" -ownedGroups: "Grupo próprio" invites: "Convidar" invitations: "Convidar" tags: "Etiquetas" @@ -475,11 +468,6 @@ file: "Ficheiros" _email: _follow: title: "Você tem um novo seguidor" -_mfm: - mention: "Menção" - quote: "Citar" - emoji: "Emoji personalizado" - search: "Buscar" _theme: keys: mention: "Menção" @@ -527,12 +515,9 @@ _notification: youGotMention: "{name} te mencionou" youGotReply: "{name} te respondeu" youGotQuote: "{name} te citou" - youGotMessagingMessageFromUser: "{name} te mandou uma mensagem de bate-papo" - youGotMessagingMessageFromGroup: "Uma mensagem foi mandada para o grupo {name}" youWereFollowed: "Você tem um novo seguidor" youReceivedFollowRequest: "Você recebeu um pedido de seguimento" yourFollowRequestAccepted: "Seu pedido de seguimento foi aceito" - youWereInvitedToGroup: "{userName} te convidou para um grupo" pollEnded: "Os resultados da enquete agora estão disponíveis" emptyPushNotificationMessage: "As notificações de alerta foram atualizadas" _types: @@ -546,7 +531,6 @@ _notification: pollEnded: "Enquetes terminando" receiveFollowRequest: "Recebeu pedidos de seguimento" followRequestAccepted: "Aceitou pedidos de seguimento" - groupInvited: "Convites de grupo" app: "Notificações de aplicativos conectados" _actions: followBack: "te seguiu de volta" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index f354801d5c..10cb085f3f 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -382,12 +382,9 @@ about: "Despre" aboutMisskey: "Despre Misskey" administrator: "Administrator" token: "Token" -twoStepAuthentication: "Autentificare în doi pași" moderator: "Moderator" nUsersMentioned: "Menționat de {n} utilizatori" securityKey: "Cheie de securitate" -securityKeyName: "Numele cheii" -registerSecurityKey: "Înregistrează o cheie de securitate" lastUsed: "Ultima utilizată" unregister: "Dezînregistrează" passwordLessLogin: "Autentificare fără parolă" @@ -405,24 +402,15 @@ markAsReadAllTalkMessages: "Marchează toate mesajele drept citit" help: "Ajutor" inputMessageHere: "Introdu un mesaj aici" close: "Închide" -group: "Grup" -groups: "Grupuri" -createGroup: "Crează un grup" -ownedGroups: "Grupuri deținute" -joinedGroups: "Grupuri alăturate" invites: "Invită" -groupName: "Numele grupului" members: "Membri" transfer: "Transferă" -messagingWithUser: "Chat privat" -messagingWithGroup: "Chat de grup" title: "Titlu" text: "Text" enable: "Activează" next: "Următorul" retype: "Introdu din nou" noteOf: "Notă de {user}" -inviteToGroup: "Invită în grup" quoteAttached: "Citat" quoteQuestion: "Vrei să adaugi ca citat?" noMessagesYet: "Niciun mesaj încă" @@ -444,18 +432,13 @@ passwordMatched: "Se potrivește!" passwordNotMatched: "Nu se potrivește" signinWith: "Autentifică-te cu {x}" signinFailed: "Nu se poate autentifica. Numele de utilizator sau parola introduse sunt incorecte." -tapSecurityKey: "Apasă pe cheia ta de securitate." or: "Sau" language: "Limbă" uiLanguage: "Limba interfeței" -groupInvited: "Ai fost invitat într-un grup" aboutX: "Despre {x}" disableDrawer: "Nu folosi meniuri în stil sertar" -youHaveNoGroups: "Nu ai niciun grup" -joinOrCreateGroup: "Primește o invitație într-un grup sau creează unul nou." noHistory: "Nu există istoric" signinHistory: "Istoric autentificări" -disableAnimatedMfm: "Dezactivează MFM cu animații" doing: "Se procesează..." category: "Categorie" tags: "Etichete" @@ -655,11 +638,6 @@ _role: _email: _follow: title: "te-a urmărit" -_mfm: - mention: "Mențiune" - quote: "Citează" - emoji: "Emoji personalizat" - search: "Caută" _theme: description: "Descriere" keys: @@ -670,6 +648,8 @@ _sfx: note: "Note" notification: "Notificări" chat: "Chat" +_ago: + invalid: "Nu e nimic de văzut aici" _widgets: profile: "Profil" instanceInfo: "Informații despre instanță" @@ -705,7 +685,6 @@ _pages: image: "Imagini" _notification: youWereFollowed: "te-a urmărit" - youWereInvitedToGroup: "Ai fost invitat într-un grup" _types: follow: "Urmărești" mention: "Mențiune" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 26e9ad0e94..81ea01179b 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -8,7 +8,7 @@ search: "Поиск" notifications: "Уведомления" username: "Имя пользователя" password: "Пароль" -forgotPassword: "Пароль забыт" +forgotPassword: "Забыли пароль?" fetchingAsApObject: "Приём с других сайтов" ok: "Окей" gotIt: "Ясно!" @@ -103,6 +103,8 @@ renoted: "Репост совершён." cantRenote: "Это нельзя репостить." cantReRenote: "Невозможно репостить репост." quote: "Цитата" +inChannelRenote: "В канале" +inChannelQuote: "Заметки в канале" pinnedNote: "Закреплённая заметка" pinned: "Закрепить в профиле" you: "Вы" @@ -129,6 +131,7 @@ unblockConfirm: "Разблокировать этот аккаунт?" suspendConfirm: "Заморозить этот аккаунт?" unsuspendConfirm: "Разморозить этот аккаунт?" selectList: "Выберите список" +selectChannel: "Выберите канал" selectAntenna: "Выберите антенну" selectWidget: "Выберите виджет" editWidgets: "Редактировать виджеты" @@ -256,6 +259,8 @@ noMoreHistory: "История закончилась" startMessaging: "Начать общение" nUsersRead: "Прочитали {n}" agreeTo: "Я соглашаюсь с {0}" +agreeBelow: "Согласен со следующими" +basicNotesBeforeCreateAccount: "Записи, перед созданием аккаунта" tos: "Пользовательское соглашение" start: "Начать" home: "Главная" @@ -351,6 +356,8 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "Включить reCAPTCHA" recaptchaSiteKey: "Ключ сайта" recaptchaSecretKey: "Секретный ключ" +turnstile: "Сервис Turnstile" +enableTurnstile: "Включить Turnstile" turnstileSiteKey: "Ключ сайта" turnstileSecretKey: "Секретный ключ" avoidMultiCaptchaConfirm: "Несколько способов проверки могут мешать друг другу. Подтвердите, если хотите отключить другие способы. Или нажмите «Отмена», чтобы оставить их включёнными." @@ -386,16 +393,19 @@ about: "Описание" aboutMisskey: "О Misskey" administrator: "Администратор" token: "Токен" -twoStepAuthentication: "Двухфакторная аутентификация" +2fa: "2-х факторная аутентификация" +totp: "Приложение-аутентификатор" +totpDescription: "Описание приложения-аутентификатора" moderator: "Модератор" moderation: "Модерация" nUsersMentioned: "Упомянуло пользователей: {n}" +securityKeyAndPasskey: "Ключ безопасности и парольная фраза" securityKey: "Ключ безопасности" -securityKeyName: "Имя ключа" -registerSecurityKey: "Зарегистрировать защитный ключ" lastUsed: "Последнее использование" +lastUsedAt: "Последнее использование: {t}" unregister: "Отписаться" passwordLessLogin: "Настроить вход без пароля" +passwordLessLoginDescription: "Вход без пароля" resetPassword: "Сброс пароля:" newPasswordIs: "Новый пароль — «{password}»." reduceUiAnimation: "Уменьшить анимацию в пользовательском интерфейсе" @@ -410,24 +420,15 @@ markAsReadAllTalkMessages: "Отметить все реплики как про help: "Помощь" inputMessageHere: "Введите сообщение здесь" close: "Закрыть" -group: "Группа" -groups: "Группы" -createGroup: "Создать группу" -ownedGroups: "Собственные группы" -joinedGroups: "Участие в группах" invites: "Приглашения" -groupName: "Название группы" members: "Участники" transfer: "Отдать" -messagingWithUser: "Общение с другим пользователем" -messagingWithGroup: "Общение в группе" title: "Заголовок" text: "Текст" enable: "Включить" next: "Дальше" retype: "Введите ещё раз" noteOf: "Что пишет {user}" -inviteToGroup: "Пригласить в группу" quoteAttached: "Цитата" quoteQuestion: "Хотите добавить цитату?" noMessagesYet: "Пока ни одного сообщения" @@ -449,20 +450,17 @@ passwordMatched: "Совпали" passwordNotMatched: "Не совпадают" signinWith: "Использовать {x} для входа" signinFailed: "Невозможно войти в систему. Введенное вами имя пользователя или пароль неверны." -tapSecurityKey: "Нажмите на свой электронный ключ" or: "или" language: "Язык" uiLanguage: "Язык интерфейса" -groupInvited: "Приглашение в группу" aboutX: "Описание {x}" emojiStyle: "Стиль эмодзи" native: "Системные" disableDrawer: "Не использовать выдвижные меню" -youHaveNoGroups: "У вас нет ни одной группы" -joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные" noHistory: "История пока пуста" signinHistory: "Журнал посещений" -disableAnimatedMfm: "Отключение анимированной разметки MFM" +enableAdvancedMfm: "Включить расширенный MFM" +enableAnimatedMfm: "Включить анимированную разметку MFM" doing: "В процессе" category: "Категория" tags: "Метки" @@ -781,6 +779,7 @@ popularPosts: "Популярные публикации" shareWithNote: "Поделиться заметкой" ads: "Реклама" expiration: "Опрос длится" +startingperiod: "Начальный период" memo: "Памятка" priority: "Приоритет" high: "Высокий" @@ -813,6 +812,7 @@ lastCommunication: "Последнее сообщение" resolved: "Решено" unresolved: "Без решения" breakFollow: "Отписка" +breakFollowConfirm: "Удалить из подписок пользователя ?" itsOn: "Включено" itsOff: "Выключено" emailRequiredForSignup: "Для регистрации учётной записи нужен адрес электронной почты" @@ -832,8 +832,6 @@ deleteAccountConfirm: "Учётная запись будет безвозвра incorrectPassword: "Пароль неверен." voteConfirm: "Отдать голос за «{choice}»?" hide: "Спрятать" -leaveGroup: "Покинуть группу" -leaveGroupConfirm: "Покинуть группу «{name}»?" useDrawerReactionPickerForMobile: "Выдвижная палитра на мобильном устройстве" welcomeBackWithName: "С возвращением, {name}!" clickToFinishEmailVerification: "Пожалуйста, нажмите [{ok}], чтобы завершить подтверждение адреса электронной почты." @@ -849,6 +847,7 @@ instanceDefaultLightTheme: "Светлая тема по умолчанию" instanceDefaultDarkTheme: "Темная тема по умолчанию" instanceDefaultThemeDescription: "Описание темы по умолчанию для инстанса" mutePeriod: "Продолжительность скрытия" +period: "Опрос длится" indefinitely: "вечно" tenMinutes: "10 минут" oneHour: "1 час" @@ -856,8 +855,11 @@ oneDay: "1 день" oneWeek: "1 неделя" reflectMayTakeTime: "Изменения могут занять время для отображения" failedToFetchAccountInformation: "Не удалось получить информацию об аккаунте" +rateLimitExceeded: "Ограничение скорости превышено" cropImage: "Кадрирование" cropImageAsk: "Нужно ли кадрировать изображение?" +cropYes: "Обрезать" +cropNo: "Не обрезать" file: "Файлы" recentNHours: "Последние {n} ч" recentNDays: "Последние {n} сут" @@ -884,6 +886,8 @@ refreshInterval: "Интервал перезагрузки" label: "Метка" type: "Тип" speed: "Скорость" +slow: "Медленная" +fast: "Быстрая" sensitiveMediaDetection: "Определение содержимого деликатного характера" localOnly: "Локально" remoteOnly: "Только удалённо" @@ -934,6 +938,20 @@ cannotPerformTemporaryDescription: "Это действие временно н preset: "Шаблоны" selectFromPresets: "Выбрать из шаблонов" achievements: "Достижения" +gotInvalidResponseError: "Сервер ответил ошибкой" +gotInvalidResponseErrorDescription: "Сервер временно не доступен. Возможно проводятся технические работы, или сервер отключен." +thisPostMayBeAnnoying: "Это сообщение может быть неприятным." +thisPostMayBeAnnoyingHome: "Этот пост может быть отправлен на главную" +thisPostMayBeAnnoyingCancel: "Этот пост не может быть отменен." +thisPostMayBeAnnoyingIgnore: "Этот пост может быть проигнорирован " +collapseRenotes: "Свернуть репосты" +internalServerError: "Внутренняя ошибка сервера" +internalServerErrorDescription: "Внутри сервера произошла непредвиденная ошибка." +copyErrorInfo: "Скопировать код ошибки" +joinThisServer: "Присоединяйтесь к этому серверу" +exploreOtherServers: "Искать другие сервера" +letsLookAtTimeline: "Давайте посмотрим на ленту" +disableFederationWarn: "Объединение отключено. Если вы отключите это, сообщение не будет приватным. В большинстве случаев вам не нужно включать эту опцию." _achievements: earnedAt: "Разблокировано в" _types: @@ -1190,6 +1208,9 @@ _role: baseRole: "Шаблон роли" useBaseValue: "Использовать значение из шаблона" chooseRoleToAssign: "Выберите роль, которую хотите выдать" + iconUrl: "Адрес на иконку роли" + asBadge: "Показывать как значок" + descriptionOfAsBadge: "Описание значка" canEditMembersByModerator: "Могут назначать модераторы" descriptionOfCanEditMembersByModerator: "Если включено, на эту роль могут назначать пользователей как администраторы, так и модераторы. Если выключено, назначать могут только администраторы." priority: "Приоритет" @@ -1207,6 +1228,7 @@ _role: pinMax: "Доступное количество закреплённых заметок" antennaMax: "Доступное количество антенн" wordMuteMax: "Доступное количество знаков в списке скрытия слов" + webhookMax: "Максимум web-хуков" clipMax: "Максимальное количество подборок" noteEachClipsMax: "Максимальное количество заметок в подборке" userListMax: "Максимальное количество списков аккаунтов" @@ -1228,7 +1250,12 @@ _role: not: "Кроме тех, у кого…" _sensitiveMediaDetection: description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно." + sensitivity: "Чувствительность обнаружения" + sensitivityDescription: "Более низкая чувствительность уменьшает количество ложных срабатываний (false positives). Повышение чувствительности уменьшает утечку при обнаружении (ложноотрицательные результаты)." setSensitiveFlagAutomatically: "Установить флаг NSFW" + setSensitiveFlagAutomaticallyDescription: "Даже если этот параметр отключен, результат оценки сохраняется внутри системы." + analyzeVideos: "Анализировать видео?" + analyzeVideosDescription: "Анализируйте видео в дополнение к неподвижным изображениям. Нагрузка на сервер немного увеличивается." _emailUnavailable: used: "Уже используется" format: "Неверный формат" @@ -1309,72 +1336,6 @@ _nsfw: respect: "Скрывать содержимое не для всех" ignore: "Показывать содержимое не для всех" force: "Скрывать вообще все файлы" -_mfm: - cheatSheet: "Подсказка по разметке MFM" - intro: "MFM — язык оформления текста, который придуман специально для Misskey и готов для применения во многих местах. На этой странице собраны и кратко изложены способы его использовать." - dummy: "Misskey расширяет границы Федиверса." - mention: "Упоминание" - mentionDescription: "При помощи знака «собака» перед именем можно упомянуть какого-нибудь пользователя." - hashtag: "Хэштег" - hashtagDescription: "При помощи знака «решётка» перед словом задаётся хэштег." - url: "Простая ссылка (URL)" - urlDescription: "Ссылки могут отображаться непосредственно." - link: "Ссылка с пояснением" - linkDescription: "Можно ссылку оформить в виде произвольного текста." - bold: "Жирный шрифт" - boldDescription: "Выделяет текст, делая буквы жирнее." - small: "Мелкий шрифт" - smallDescription: "Делает текст маленьким и незаметным." - center: "Выровнять элементы по центру" - centerDescription: "Так можно выровнять что-то по центру." - inlineCode: "Программа (в тексте)" - inlineCodeDescription: "Подсвечивает фрагмент программы внутри сплошного текста." - blockCode: "Программа (блок)" - blockCodeDescription: "Оформляет текст программы в виде отдельного блокоа. Он может состоять из множества строк." - inlineMath: "Математическое выражение (в тексте)" - inlineMathDescription: "Позволяет вставлять математические выражения внутрь текста при помощи языка KaTeX." - blockMath: "Математическое выражение (блок)" - blockMathDescription: "Оформляет математическое выражение (KaTeX) на отдельной строке." - quote: "Цитата" - quoteDescription: "Так можно процитировать чей-то текст." - emoji: "Собственные эмодзи" - emojiDescription: "Можно вставить эмодзи в текст, окружив название двоеточиями." - search: "Поиск" - searchDescription: "Можно добавить форму для поиска, сразу задав, что искать." - flip: "Переворот" - flipDescription: "Позволяет отразить текст зеркально по вертикали или горизонтали." - jelly: "Анимация желе (шлёп-плёп)" - jellyDescription: "Напоминает горку джема, дёргающуюся от шлепков." - tada: "Анимация (та-дам!)" - tadaDescription: "Получается нечто выпрыгивающее, как бы крича: «а вот и я!»" - jump: "Анимация прыжков (прыг-скок)" - jumpDescription: "Побуждает радостно подпрыгивать." - bounce: "Анимация отскоков (бум-бум)" - bounceDescription: "Это будет скакать как мяч." - shake: "Анимация дрожи (б-р-р-р)" - shakeDescription: "Такое дрожит, словно от холода. Или от страха." - twitch: "Анимация тряски" - twitchDescription: "Заставляет трястись как одержимого" - spin: "Вращение" - spinDescription: "Так можно крутить содержимое в разных направлениях." - x2: "Крупный шрифт" - x2Description: "Увеличивает содержимое." - x3: "Ещё крупнее" - x3Description: "Сильнее увеличивает содержимое." - x4: "Совсем крупно" - x4Description: "Увеличивает содержимое совсем сильно." - blur: "Размытие" - blurDescription: "Размывает текст до нечитаемости, будто его поместили за матовое стекло. Наведение указателя мыши на размытый текст возвращает чёткость." - font: "Шрифт" - fontDescription: "Так можно писать произвольным шрифтом." - rainbow: "Радуга" - rainbowDescription: "Заставлять содержимое отображаться в цветах радуги." - sparkle: "Искры" - sparkleDescription: "Добавляет эффект искрящихся частиц." - rotate: "Повернуть" - rotateDescription: "Поворачивает на заданный угол." - plain: "Буквально" - plainDescription: "MFM внутри отключается, и текст отображается как есть" _instanceTicker: none: "Не показывать" remote: "Только для других сайтов" @@ -1504,6 +1465,7 @@ _ago: weeksAgo: "{n} нед. назад" monthsAgo: "{n} мес. назад" yearsAgo: "{n} г. назад" + invalid: "Ничего нет" _time: second: "с" minute: "мин" @@ -1537,14 +1499,29 @@ _tutorial: step8_3: "Эту настройку вы всегда сможете поменять" _2fa: alreadyRegistered: "Двухфакторная аутентификация уже настроена." - registerDevice: "Зарегистрируйте ваше устройство" - registerKey: "Зарегистрировать ключ" + registerTOTP: "Начните настраивать приложение-аутентификатор" + passwordToTOTP: "Пожалуйста, введите свой пароль" step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}." step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." + step2Click: "Нажав на QR-код, вы можете зарегистрироваться с помощью приложения для аутентификации или брелка для ключей, установленного на вашем устройстве." step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):" + step3Title: "Введите проверочный код" step3: "И наконец, введите код, который покажет приложение." step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом." + securityKeyNotSupported: "Ваш браузер не поддерживает ключи безопасности." + registerTOTPBeforeKey: "Чтобы зарегистрировать ключ безопасности и пароль, сначала настройте приложение аутентификации." securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве." + chromePasskeyNotSupported: "В настоящее время Chrome не поддерживает пароль-ключи." + registerSecurityKey: "Зарегистрируйте ключ безопасности ・Passkey" + securityKeyName: "Введите имя для ключа" + tapSecurityKey: "Пожалуйста, следуйте инструкциям в вашем браузере, чтобы зарегистрировать свой ключ безопасности или пароль" + removeKey: "Удалить ключ безопасности" + removeKeyConfirm: "Удалить резервную копию «{name}»?" + whyTOTPOnlyRenew: "Если ключ безопасности зарегистрирован, вы не сможете отключить приложение аутентификации." + renewTOTP: "Перенастроите приложение аутентификации" + renewTOTPConfirm: "Проверочный код предыдущего приложения для аутентификации больше не будет доступен" + renewTOTPOk: "Настроить" + renewTOTPCancel: "Нет, спасибо" _permissions: "read:account": "Просматривать данные учётной записи" "write:account": "Изменять данные учётной записи" @@ -1579,18 +1556,20 @@ _permissions: "read:gallery-likes": "Просмотр списка понравившегося в галерее" "write:gallery-likes": "Изменение списка понравившегося в галерее" _auth: + shareAccessTitle: "Разрешения для приложений" shareAccess: "Дать доступ для «{name}» к вашей учётной записи?" shareAccessAsk: "Уверены, что хотите дать приложению доступ к своей учётной записи?" + permission: "{name} Запрашивает следующие разрешения:" permissionAsk: "Приложение запрашивает следующие разрешения:" pleaseGoBack: "Вернитесь, пожалуйста, в приложение" callback: "Возврат в приложение" denied: "Доступ закрыт" + pleaseLogin: "Вы должны войти в систему, чтобы дать разрешение приложению." _antennaSources: all: "Все заметки" homeTimeline: "Заметки тех на которых вы подписаны" users: "Заметки выбранных пользователей" userList: "Заметки пользователей из выбранных списков" - userGroup: "Заметки от пользователей из заданной группы" _weekday: sunday: "Воскресенье" monday: "Понедельник" @@ -1665,8 +1644,8 @@ _visibility: followersDescription: "Только вашим подписчикам" specified: "Личное" specifiedDescription: "Тем, кого укажете" - localOnly: "Локально" - localOnlyDescription: "Только для этого сайта" + disableFederation: "Отключить федерацию" + disableFederationDescription: "Не доставляет в другие экземпляры" _postForm: replyPlaceholder: "Ответ на заметку..." quotePlaceholder: "Пояснение к цитате..." @@ -1731,7 +1710,16 @@ _timelines: social: "Социальная" global: "Всеобщая" _play: + new: "Создать приложение " + edit: "Редактировать приложение" + created: "Приложение создано" + updated: "Приложение обновлено" + deleted: "Приложение удалено" + pageSetting: "Настройки приложения" + editThisPage: "Отредактировать страницу" viewSource: "Просмотр исходника" + my: "Мои приложения " + liked: "Понравилось" featured: "Популярные" title: "Заголовок" script: "Скрипт" @@ -1795,13 +1783,11 @@ _notification: youGotReply: "{name} отвечает вам." youGotQuote: "{name} цитирует вас." youRenoted: "{name} передаёт вашу заметку." - youGotMessagingMessageFromUser: "{name} пишет вам." - youGotMessagingMessageFromGroup: "Новое сообщение в группе «{name}»." youWereFollowed: "У вас новый подписчик." youReceivedFollowRequest: "У вас новый запрос на подписку." yourFollowRequestAccepted: "Ваш запрос на подписку одобрен." - youWereInvitedToGroup: "Вы приглашены в группу." pollEnded: "Подведены окончательные итоги опроса" + unreadAntennaNote: "Антенна {name}" emptyPushNotificationMessage: "Обновлены push-уведомления" achievementEarned: "Получено достижение" _types: @@ -1815,7 +1801,7 @@ _notification: pollEnded: "Окончания опросов" receiveFollowRequest: "Получен запрос на подписку" followRequestAccepted: "Запрос на подписку одобрен" - groupInvited: "Приглашение в группы" + achievementEarned: "Получение достижений" app: "Уведомления из приложений" _actions: followBack: "отвечает взаимной подпиской" @@ -1848,3 +1834,6 @@ _deck: channel: "Каналы" mentions: "Упоминания" direct: "Личное" +_dialog: + charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}" + charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 369f1af36d..d4be5540b8 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -103,6 +103,8 @@ renoted: "Preposlané." cantRenote: "Tento príspevok sa nedá preposlať." cantReRenote: "Odpoveď nemôže byť odstránená." quote: "Citovať" +inChannelRenote: "Preposlania v kanáli" +inChannelQuote: "Citácie v kanáli" pinnedNote: "Pripnuté poznámky" pinned: "Pripnúť" you: "Vy" @@ -129,6 +131,7 @@ unblockConfirm: "Naozaj chcete odblokovať tento účet?" suspendConfirm: "Naozaj chcete zmraziť tento účet?" unsuspendConfirm: "Naozaj chcete odmraziť tento účet?" selectList: "Vyberte zoznam" +selectChannel: "Zvoľte kanál" selectAntenna: "Vyberte anténu" selectWidget: "Vyberte widget" editWidgets: "Upraviť widget" @@ -256,6 +259,8 @@ noMoreHistory: "To je všetko" startMessaging: "Začať chat" nUsersRead: "prečítané {n} používateľmi" agreeTo: "Súhlasím s {0}" +agreeBelow: "Súhlasím s nasledovným" +basicNotesBeforeCreateAccount: "Základné bezpečnostné opatrenia" tos: "Podmienky používania" start: "Začať" home: "Domov" @@ -388,16 +393,19 @@ about: "Informácie" aboutMisskey: "O Misskey" administrator: "Administrátor" token: "Token" -twoStepAuthentication: "Dvojfaktorová autentifikácia" +2fa: "Dvojfaktorové overenie (2FA)" +totp: "Overovacia aplikácia" +totpDescription: "Zadajte jednorazové heslo z overovacej aplikácie" moderator: "Moderátor" moderation: "Moderovanie" nUsersMentioned: "{n} používateľov spomenulo" +securityKeyAndPasskey: "Bezpečnostný kľúč/heslo" securityKey: "Bezpečnostný kľúč" -securityKeyName: "Názov kľúča" -registerSecurityKey: "Registrovať bezpečnostný kľúč" lastUsed: "Naposledy použité" +lastUsedAt: "Naposledy použité: {t}" unregister: "Odregistrovať" passwordLessLogin: "Nastaviť bezheslové prihlásenie" +passwordLessLoginDescription: "Prihlásenie bez hesla, len bezpečnostným kľúčom alebo prístupovým kľúčom" resetPassword: "Resetovať heslo" newPasswordIs: "Nové heslo je \"{password}\"" reduceUiAnimation: "Menej UI animácií" @@ -412,24 +420,15 @@ markAsReadAllTalkMessages: "Označiť všetky správy ako prečítané" help: "Pomoc" inputMessageHere: "Sem napíšte správu" close: "Zavrieť" -group: "Skupina" -groups: "Skupiny" -createGroup: "Vytvoriť skupinu" -ownedGroups: "Vlastnené skupiny" -joinedGroups: "Členstvo v skupinách" invites: "Pozvať" -groupName: "Názov skupiny" members: "Členovia" transfer: "Presun" -messagingWithUser: "Súkromný chat" -messagingWithGroup: "Skupinový chat" title: "Nadpis" text: "Text" enable: "Povoliť" next: "Ďalší" retype: "Zadajte znovu" noteOf: "Poznámky používateľa {user}" -inviteToGroup: "Pozvať do skupiny" quoteAttached: "Citované" quoteQuestion: "Pripojiť ako citát?" noMessagesYet: "Zatiaľ žiadne správy" @@ -451,20 +450,18 @@ passwordMatched: "Heslá sú rovnaké" passwordNotMatched: "Heslá nie sú rovnaké" signinWith: "Prihlásiť sa použitím {x}" signinFailed: "Nedá sa prihlásiť. Skontrolujte prosím meno používateľa a heslo." -tapSecurityKey: "Ťuknite na bezpečnostný kľúč" or: "Alebo" language: "Jazyk" uiLanguage: "Jazyk používateľského prostredia" -groupInvited: "Pozvať do skupiny" aboutX: "O {x}" emojiStyle: "Štýl emoji" native: "Natívne" disableDrawer: "Nepoužívať šuflíkové menu" -youHaveNoGroups: "Nemáte žiadne skupiny" -joinOrCreateGroup: "Požiadajte o pozvanie do existujúcej skupiny alebo vytvorte novú." +showNoteActionsOnlyHover: "Ovládacie prvky poznámky sa zobrazujú len po nabehnutí myši" noHistory: "Žiadna história" signinHistory: "História prihlásení" -disableAnimatedMfm: "Vypnúť MFM s animáciou" +enableAdvancedMfm: "Povolenie pokročilého MFM" +enableAnimatedMfm: "Povoliť animované MFM" doing: "Pracujem..." category: "Kategórie" tags: "Značky" @@ -573,6 +570,7 @@ manage: "Administrácia" plugins: "Pluginy" preferencesBackups: "Zálohy nastavení" deck: "Deck" +undeck: "Oddokovať" useBlurEffectForModal: "Použiť efekt rozmazania na okná" useFullReactionPicker: "Použiť plnú veľkosť výberu reakcií" width: "Šírka" @@ -782,6 +780,7 @@ popularPosts: "Populárne príspevky" shareWithNote: "Zdieľať s poznámkou" ads: "Reklamy" expiration: "Ukončiť hlasovanie" +startingperiod: "Začiatok" memo: "Memo" priority: "Priorita" high: "Vysoká" @@ -833,8 +832,6 @@ deleteAccountConfirm: "Toto nezvrátiteľne vymaže váš účet. Pokračovať?" incorrectPassword: "Nesprávne heslo." voteConfirm: "Potvrdzujete svoj hlas za \"{choice}\"?" hide: "Skryť" -leaveGroup: "Opustiť skupiny" -leaveGroupConfirm: "Naozaj chcete opustiť \"{name}\"?" useDrawerReactionPickerForMobile: "Zobraziť výber reakcií ako šuflík na mobile" welcomeBackWithName: "Vitajte späť, {name}" clickToFinishEmailVerification: "Kliknutím na [{ok}] dokončíte overeniu emailu." @@ -850,11 +847,13 @@ instanceDefaultLightTheme: "Predvolená svetlá téma" instanceDefaultDarkTheme: "Predvolená tmavá téma" instanceDefaultThemeDescription: "Vložte kód témy v objektovom formáte" mutePeriod: "Trvanie stíšenia" +period: "Ukončiť hlasovanie" indefinitely: "Navždy" tenMinutes: "10 minút" oneHour: "1 hodina" oneDay: "1 deň" oneWeek: "1 týždeň" +oneMonth: "1 mesiac" reflectMayTakeTime: "Zmeny môžu chvíľu trvať kým sa prejavia." failedToFetchAccountInformation: "Nepodarilo sa načítať informácie o účte." rateLimitExceeded: "Prekročený limit rýchlosti" @@ -1013,72 +1012,6 @@ _nsfw: respect: "Skryť NSFW médiá" ignore: "Neskrývať NSFW médiá" force: "Skryť všetky médiá" -_mfm: - cheatSheet: "MFM Cheatsheet" - intro: "MFM je Misskey exkluzívny značkovací jazyk, ktorý sa dá používať na viacerých miestach. Tu môžete vidieť zoznam všetkej dostupnej MFM syntaxe." - dummy: "Misskey rozširuje svet Fediverza" - mention: "Zmienka" - mentionDescription: "Používateľa spomeniete použítím zavináča a mena používateľa" - hashtag: "Hashtag" - hashtagDescription: "Môžete zadať hashtag použitím mriežky a textu" - url: "URL" - urlDescription: "URL sa dajú zobraziť." - link: "Odkaz" - linkDescription: "Jednotlivé časti texty sa dajú zobraziť ako URL." - bold: "Tučné" - boldDescription: "Zvýrazní písmená tým, že budú tučnejšie." - small: "Malé" - smallDescription: "Zobrazí obsah malý a tenký." - center: "Vystrediť prvky" - centerDescription: "Zobrazí obsah v strede" - inlineCode: "Kód (inline)" - inlineCodeDescription: "Zobrazí kód so zvýraznením syntaxe." - blockCode: "Kód (blok)" - blockCodeDescription: "Zobrazí viacriadkový kód so zvýraznením syntaxe v bloku." - inlineMath: "Vzorec (inline)" - inlineMathDescription: "Zobrazí matematický vzorec (KaTeX) v riadku." - blockMath: "Vzorec (blok)" - blockMathDescription: "Zobrazí viacriadkový matematický vzorec (KaTeX) v bloku" - quote: "Citovať" - quoteDescription: "Zobrazí obsah ako citát." - emoji: "Vlastné emoji" - emojiDescription: "Pridaním dvojbodiek pred a za názov vlastnej emoji, sa dá zobraziť vlastná emoji." - search: "Hľadať" - searchDescription: "Zobrazí vyhľadávacie pole so zadaným textom." - flip: "Preklopiť" - flipDescription: "Preklopí obsah horizontálne alebo vertikálne" - jelly: "Animácia (želé)" - jellyDescription: "Obsah sa bude hýbať ako želé." - tada: "Animácia (tadá)" - tadaDescription: "Obsah sa bude hýbať ako Tada!" - jump: "Animácia (skok)" - jumpDescription: "Obsah skočí." - bounce: "Animácia (odraz)" - bounceDescription: "Obsah sa bude odrážať." - shake: "Animácia (trasenie)" - shakeDescription: "Obsah sa bude triasť." - twitch: "Animácia (myknutie)" - twitchDescription: "Obsahu dá animáciu silného trasenia." - spin: "Animácia (rotácia)" - spinDescription: "Obsahu pridá otáčajúcu animáciu." - x2: "Veľký" - x2Description: "Zobrazí obsah väčší." - x3: "Veľmi veľký" - x3Description: "Zobrazí obsah ešte väčší." - x4: "Neuveriteľne veľký" - x4Description: "Zobrazí obsah ešte viac veľký než veľmi veľký." - blur: "Rozmazanie" - blurDescription: "Týmto efektom môže byť obsah rozmazaný. Zaostrí sa keď ned neho príde kurzor." - font: "Písmo" - fontDescription: "Nastaví písmo, ktorým sa zobrazí text." - rainbow: "Dúha" - rainbowDescription: "Zobrazí obsah vo farbách dúhy." - sparkle: "Trblietky" - sparkleDescription: "Obsahu dodá trblietajúci efekt." - rotate: "Otáčať" - rotateDescription: "Otočí obsah o určitý uhol." - plain: "Obyčajné" - plainDescription: "Bez akejkoľvej syntaxe" _instanceTicker: none: "Nikdy nezobrazovať" remote: "Zobraziť pre vzdialených používateľov" @@ -1208,6 +1141,7 @@ _ago: weeksAgo: "pred {n} týždňami" monthsAgo: "pred {n} mesiacmi" yearsAgo: "pred {n} rokmi" + invalid: "Nič tu nie je" _time: second: "s" minute: "min" @@ -1241,14 +1175,14 @@ _tutorial: step8_3: "Nastavenia notifikácií môžete neskôr zmeniť." _2fa: alreadyRegistered: "Už ste zaregistrovali 2-faktorové autentifikačné zariadenie." - registerDevice: "Registrovať nové zariadenie" - registerKey: "Registrovať bezpečnostný kľúč" step1: "Najprv si nainštalujte autentifikačnú aplikáciu (napríklad {a} alebo {b}) na svoje zariadenie." step2: "Potom, naskenujte QR kód zobrazený na obrazovke." step2Url: "Do aplikácie zadajte nasledujúcu URL adresu:" step3: "Nastavenie dokončíte zadaním tokenu z vašej aplikácie." step4: "Od teraz, všetky ďalšie prihlásenia budú vyžadovať prihlasovací token." securityKeyInfo: "Okrem odtlačku prsta alebo PIN autentifikácie si môžete nastaviť autentifikáciu cez hardvérový bezpečnostný kľúč podporujúci FIDO2 a tak ešte viac zabezpečiť svoj účet." + removeKeyConfirm: "Naozaj chcete odstrániť \"{name}\"?" + renewTOTPCancel: "Nie, ďakujem" _permissions: "read:account": "Vidieť informácie o vašom účte" "write:account": "Upraviť informácie o vašom účte" @@ -1294,7 +1228,6 @@ _antennaSources: homeTimeline: "Poznámky od sledovaného používateľa" users: "Poznámky od konkrétneho používateľa" userList: "Poznámky od používateľov v zozname" - userGroup: "Poznámky od používateľov z konkrétnej skupiny." _weekday: sunday: "Nedeľa" monday: "Pondelok" @@ -1366,8 +1299,6 @@ _visibility: followersDescription: "Viditeľné iba tým, ktorí vás sledujú" specified: "Priame" specifiedDescription: "Viditeľné iba pre konkrétnych používateľov" - localOnly: "Iba lokálne" - localOnlyDescription: "Vzdialený používatelia nebudú vidieť" _postForm: replyPlaceholder: "Odpoveď na túto poznámku..." quotePlaceholder: "Citovanie tejto poznámky..." @@ -1495,12 +1426,9 @@ _notification: youGotReply: "{name} vám odpovedal/a" youGotQuote: "{name} vás citoval/a" youRenoted: "{name} preposlal/a vašu poznámku" - youGotMessagingMessageFromUser: "{name} vám poslal/a správu" - youGotMessagingMessageFromGroup: "Prišla správa do skupiny {name}" youWereFollowed: "Máte nového sledujúceho" youReceivedFollowRequest: "Dostali ste žiadosť o sledovanie" yourFollowRequestAccepted: "Vaša žiadosť o sledovanie bola prijatá" - youWereInvitedToGroup: "Pozvať do skupiny" pollEnded: "Výsledky hlasovania sú k dispozícii." unreadAntennaNote: "Anténa {name}" emptyPushNotificationMessage: "Push notifikácie aktualizované" @@ -1515,7 +1443,6 @@ _notification: pollEnded: "Hlasovanie skončilo" receiveFollowRequest: "Doručené žiadosti o sledovanie" followRequestAccepted: "Schválené žiadosti o sledovanie" - groupInvited: "Pozvánky do skupín" app: "Oznámenia z prepojených aplikácií" _actions: followBack: "Sledovať späť\n" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 1abd0d194d..5e66df2076 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -371,11 +371,6 @@ pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för _email: _follow: title: "följde dig" -_mfm: - mention: "Nämn" - quote: "Citat" - emoji: "Anpassa emoji" - search: "Sök" _channel: setBanner: "Välj banner" removeBanner: "Ta bort banner" @@ -388,12 +383,13 @@ _sfx: notification: "Notifikationer" chat: "Chatt" antenna: "Antenner" +_2fa: + renewTOTPCancel: "Nej tack" _antennaSources: all: "Alla noter" homeTimeline: "Noter från följda användare" users: "Noter från specifika användare" userList: "Noter från en specificerad lista av användare" - userGroup: "Noter från användare i en specificerad grupp" _widgets: profile: "Profil" instanceInfo: "Instansinformation" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 0f5ff62485..7dcc15824e 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -103,6 +103,8 @@ renoted: "รีโน้ตเอาไว้" cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตไว้ใหม่ได้นะ" cantReRenote: "ไม่สามารถรีโน้ตเอาไว้ใหม่ได้นะ" quote: "อ้างคำพูด" +inChannelRenote: "รีโน้ตช่องแชลแนลเท่านั้น" +inChannelQuote: "อ้างช่องเท่านั้น" pinnedNote: "โน้ตที่ปักหมุดเอาไว้" pinned: "ปักหมุดไปยังโปรไฟล์" you: "ตัวเอง" @@ -257,6 +259,8 @@ noMoreHistory: "ในนั้นไม่มีประวัติอีก startMessaging: "เริ่มการสนทนา" nUsersRead: "อ่านโดย {n}" agreeTo: "ฉันยอมรับที่จะ {0}" +agreeBelow: "ฉันยอมรับถึงด้านล่าง" +basicNotesBeforeCreateAccount: "หมายเหตุสำคัญ" tos: "ข้อกำหนดและเงื่อนไข" start: "เริ่มต้น​ใช้งาน​" home: "หน้าแรก" @@ -389,16 +393,19 @@ about: "เกี่ยวกับ" aboutMisskey: "เกี่ยวกับ Misskey" administrator: "ผู้ดูแลระบบ" token: "โทเค็น" -twoStepAuthentication: "ยืนยันตัวตน 2 ชั้น" +2fa: "การยืนยันตัวตนแบบสองชั้น" +totp: "แอป Authenticator" +totpDescription: "ใช้แอปยืนยันตัวตนเพื่อป้อนรหัสผ่านแบบใช้ครั้งเดียว" moderator: "ผู้ควบคุม" moderation: "การกลั่นกรอง" nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้" +securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน" securityKey: "กุญแจความปลอดภัย" -securityKeyName: "ชื่อคีย์" -registerSecurityKey: "ลงทะเบียนรหัสความปลอดภัยคีย์" lastUsed: "ใช้ล่าสุด" +lastUsedAt: "ใช้งานครั้งล่าสุด: {t}" unregister: "เลิกติดตาม" passwordLessLogin: "เข้าสู่ระบบแบบไม่ใช้รหัสผ่าน" +passwordLessLoginDescription: "อนุญาตให้เข้าสู่ระบบโดยไม่ต้องใช้รหัสผ่านโดยใช้รหัสรักษาความปลอดภัยหรือรหัสผ่านเท่านั้น" resetPassword: "รีเซ็ตรหัสผ่าน" newPasswordIs: "รหัสผ่านใหม่คือ \"{password}\"" reduceUiAnimation: "ลดภาพเคลื่อนไหว UI" @@ -413,24 +420,15 @@ markAsReadAllTalkMessages: "ทำเครื่องหมายข้อค help: "ช่วยเหลือ" inputMessageHere: "พิมพ์ข้อความที่นี่" close: "ปิด" -group: "กลุ่ม" -groups: "กลุ่ม" -createGroup: "สร้างกลุ่ม" -ownedGroups: "กลุ่มที่เป็นเจ้าของ" -joinedGroups: "เข้าร่วมกลุ่ม" invites: "เชิญชวน" -groupName: "ชื่อกลุ่ม" members: "สมาชิก" transfer: "ถ่ายโอน" -messagingWithUser: "แชทส่วนตัว" -messagingWithGroup: "แชทกลุ่ม" title: "หัวข้อ" text: "ข้อความ" enable: "เปิดใช้งาน" next: "ถัด​ไป" retype: "พิมพ์รหัสอีกครั้ง" noteOf: "โน้ต โดย {ผู้ใช้งาน}" -inviteToGroup: "ชวนเข้ากลุ่ม" quoteAttached: "อ้างอิง" quoteQuestion: "นายต้องการที่จะอ้างอิงหรอ?" noMessagesYet: "ยังไม่มีข้อความนะ" @@ -452,20 +450,18 @@ passwordMatched: "ถูกต้อง!" passwordNotMatched: "ไม่ถูกต้อง" signinWith: "ลงชื่อเข้าใช้ด้วย {x}" signinFailed: "ไม่สามารถลงชื่อผู้เข้าใช้ได้ เนื่องจาก ชื่อผู้ใช้หรือรหัสผ่านที่คุณป้อนนั้นไม่ถูกต้องนะ" -tapSecurityKey: "แตะคีย์ความปลอดภัย" or: "หรือ" language: "ภาษา" uiLanguage: "ภาษาอินเทอร์เฟซผู้ใช้งาน" -groupInvited: "คุณได้รับเชิญให้เข้าร่วมกลุ่ม" aboutX: "เกี่ยวกับ {x}" emojiStyle: "สไตล์อิโมจิ" native: "ภาษาแม่" disableDrawer: "อย่าใช้ลิ้นชักสไตล์เมนู" -youHaveNoGroups: "คุณยังไม่มีกลุ่ม" -joinOrCreateGroup: "รับเชิญเข้าร่วมกลุ่มหรือสร้างกลุ่มของคุณเองเลยนะ" +showNoteActionsOnlyHover: "แสดงการดำเนินการเฉพาะโน้ตเมื่อโฮเวอร์" noHistory: "ไม่มีรายการ" signinHistory: "ประวัติการเข้าสู่ระบบ" -disableAnimatedMfm: "ปิดการใช้งาน MFM ด้วยแอนิเมชั่น" +enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง" +enableAnimatedMfm: "เปิดการใช้งาน MFM ด้วยแอนิเมชั่น" doing: "กำลังประมวลผล......" category: "หมวดหมู่" tags: "แท็ก" @@ -784,6 +780,7 @@ popularPosts: "โพสต์ติดอันดับ" shareWithNote: "แบ่งปันด้วยโน้ต" ads: "โฆษณา" expiration: "กำหนดเวลา" +startingperiod: "เริ่ม" memo: "ข้อควรจำ" priority: "ลำดับความสำคัญ" high: "สูง" @@ -816,6 +813,7 @@ lastCommunication: "การสื่อสารครั้งสุดท้ resolved: "คลี่คลายแล้ว" unresolved: "รอการเฉลย" breakFollow: "ลบผู้ติดตาม" +breakFollowConfirm: "ลบผู้ติดตามนี้ออกจริงหรอ?" itsOn: "เปิดใช้งาน" itsOff: "ปิดใช้งาน" emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร" @@ -835,8 +833,6 @@ deleteAccountConfirm: "การดำเนินการนี้จะลบ incorrectPassword: "รหัสผ่านไม่ถูกต้อง" voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?" hide: "ซ่อน" -leaveGroup: "ออกจากกลุ่ม" -leaveGroupConfirm: "คุณแน่ใจหรอว่าต้องการออกจาก \"{name}\"" useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ" welcomeBackWithName: "ยินดีต้อนรับการกลับมานะค่ะ, {name}" clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ" @@ -852,16 +848,20 @@ instanceDefaultLightTheme: "ธีมสว่างค่าเริ่มต instanceDefaultDarkTheme: "ธีมมืดค่าเริ่มต้นอินสแตนซ์" instanceDefaultThemeDescription: "ป้อนรหัสธีมในรูปแบบออบเจ็กต์" mutePeriod: "ระยะเวลาปิดเสียง" +period: "สิ้นสุดการสำรวจความคิดเห็น" indefinitely: "ตลอดไป" tenMinutes: "10 นาที" oneHour: "1 ชั่วโมง" oneDay: "1 วัน" oneWeek: "1 สัปดาห์" +oneMonth: "หนึ่งเดือน" reflectMayTakeTime: "อาจจำเป็นต้องใช้เวลาสักระยะหนึ่งจึงจะเห็นแสดงผลได้นะ" failedToFetchAccountInformation: "ไม่สามารถเรียกดึงข้อมูลบัญชีได้" rateLimitExceeded: "เกินขีดจำกัดอัตรา" cropImage: "ครอบตัดรูปภาพ" cropImageAsk: "คุณต้องการครอบตัดรูปภาพนี้อย่างงั้นหรือ?" +cropYes: "ครอบตัด" +cropNo: "ใช้ตามที่เป็นอยู่" file: "ไฟล์" recentNHours: "ล่าสุด {n} ชั่วโมงที่แล้ว" recentNDays: "ล่าสุด {n} วันที่แล้ว" @@ -940,6 +940,21 @@ cannotPerformTemporaryDescription: "การดําเนินการน preset: "พรีเซ็ต" selectFromPresets: "เลือกจากการพรีเซ็ต" achievements: "ความสำเร็จ" +gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง" +gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ" +thisPostMayBeAnnoying: "โน้ตนี้อาจจะเป็นการรบกวนผู้อื่นนะคะ" +thisPostMayBeAnnoyingHome: "โพสต์ไปยังบ้านไทม์ไลน์" +thisPostMayBeAnnoyingCancel: "เลิก" +thisPostMayBeAnnoyingIgnore: "โพสต์ยังไงก็แล้วแต่" +collapseRenotes: "ยุบ renotes ที่คุณได้เห็นแล้ว" +internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด" +internalServerErrorDescription: "เซิร์ฟเวอร์รันค้นพบข้อผิดพลาดที่ไม่คาดคิด" +copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด" +joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้" +exploreOtherServers: "มองหาอินสแตนซ์อื่น" +letsLookAtTimeline: "ลองดูที่ไทม์ไลน์" +disableFederationWarn: "การดำเนินการนี้ถ้าหากจะปิดใช้งานการรวมศูนย์ แต่โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป ยกเว้นแต่ว่าจะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องใช้การตั้งค่านี้นะ" +invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ" _achievements: earnedAt: "ได้รับเมื่อ" _types: @@ -1324,72 +1339,6 @@ _nsfw: respect: "ซ่อนสื่อ NSFW" ignore: "อย่าซ่อนสื่อ NSFW" force: "ซ่อนสื่อทั้งหมด" -_mfm: - cheatSheet: "โค้ด MFM Cheat Sheet" - intro: "MFM เป็นภาษามาร์กอัปพิเศษเฉพาะของ Misskey ที่สามารถใช้ได้ในหลายที่ คุณยังสามารถดูรายการไวยากรณ์ MFM ที่มีอยู่ทั้งหมดได้ที่นี่นะ" - dummy: "Misskey ขยายโลกของ Fediverse" - mention: "กล่าวถึง" - mentionDescription: "คุณสามารถระบุผู้ใช้โดยใช้ At-Symbol และชื่อผู้ใช้ได้นะ" - hashtag: "แฮชแท็ก" - hashtagDescription: "คุณสามารถระบุชื่อแฮชแท็กได้โดยใช้เครื่องหมายตัวเลขและข้อความได้นะ" - url: "URL" - urlDescription: "สามารถแสดง URL ได้นะ" - link: "ลิงก์" - linkDescription: "เจาะจงเฉพาะ ส่วนของข้อความที่สามารถแสดงเป็น URL ได้" - bold: "ตัวหนา" - boldDescription: "ไฮไลท์ตัวอักษรโดยทำให้หนาขึ้น" - small: "ขนาดเล็ก" - smallDescription: "แสดงผลเนื้อหาขนาดเล็กและบาง" - center: "เซ็นเตอร์" - centerDescription: "แสดงผลเนื้อหาเป็นศูนย์กลาง" - inlineCode: "โค้ด (อินไลน์)" - inlineCodeDescription: "แสดงผลการเน้นไวยากรณ์แบบอินไลน์สำหรับโค้ด (โปรแกรม)" - blockCode: "โค้ด (บล็อก)" - blockCodeDescription: "แสดงผลการเน้นไวยากรณ์สำหรับโค้ดหลายบรรทัด (โปรแกรม) ในบล็อก" - inlineMath: "คณิต (อินไลน์)" - inlineMathDescription: "แสดงผลสูตรคณิต (KaTeX) ในบรรทัด" - blockMath: "คณิต (บล็อก)" - blockMathDescription: "แสดงผลสูตรคณิตหลายบรรทัด (KaTeX) ในบล็อก" - quote: "อ้างคำพูด" - quoteDescription: "แสดงผลเนื้อหาเป็นใบเสนอราคา" - emoji: "กำหนดอีโมจิเอง" - emojiDescription: "โดยล้อมรอบชื่ออีโมจิที่กำหนดเองด้วยเครื่องหมายทวิภาค จะสามารถแสดงผลอีโมจิที่กำหนดเองได้" - search: "ค้นหา" - searchDescription: "แสดงผลกล่องค้นหาพร้อมกับข้อความที่ป้อนไว้ล่วงหน้า" - flip: "พลิก" - flipDescription: "พลิกเนื้อหาในแนวนอนหรือแนวตั้ง" - jelly: "แอนิเมชั่น (เยลลี่)" - jellyDescription: "ให้เนื้อหาเป็นแอนิเมชั่นเหมือนเยลลี่" - tada: "แอนิเมชั่น (ธาดา)" - tadaDescription: "ให้เนื้อหาเป็นแอนิเมชั่นเหมือน \"ทาด้า!\"" - jump: "อนิเมชั่น (กระโดด)" - jumpDescription: "ให้เนื้อหามีภาพเคลื่อนไหวแบบกระโดด" - bounce: "อนิเมชั่น (เด้ง)" - bounceDescription: "ให้เนื้อหามีอนิเมชั่นเด้ง" - shake: "อนิเมชั่น (เขย่า)" - shakeDescription: "ให้เนื้อหามีภาพเคลื่อนไหวสั่น" - twitch: "แอนิเมชั่น (Twitch)" - twitchDescription: "ให้เนื้อหามีแอนิเมชั่นกระตุกอย่างแรง" - spin: "แอนิเมชั่น (สปิน)" - spinDescription: "ให้เนื้อหาเป็นภาพเคลื่อนไหวแบบหมุน" - x2: "ขนาดใหญ่" - x2Description: "แสดงเนื้อหาที่ใหญ่ขึ้น" - x3: "ใหญ่มาก" - x3Description: "แสดงเนื้อหาอีเว้นท์ที่ใหญ่ขึ้น" - x4: "ใหญ่อย่างไม่น่าเชื่อ" - x4Description: "แสดงผลเนื้อหาที่ใหญ่กว่าใหญ่กว่าขนาดใหญ่" - blur: "เบลอ" - blurDescription: "เบลอเนื้อหา จะแสดงผลอย่างชัดเจนต่อเมื่อวางเมาส์เหนือ" - font: "ตัวอักษร" - fontDescription: "ตั้งค่าตัวอักษรเพื่อแสดงเนื้อหาใน" - rainbow: "สายรุ้ง" - rainbowDescription: "ทำให้เนื้อหานั้นปรากฏเป็นสีรุ้ง" - sparkle: "กลิตเตอร์" - sparkleDescription: "ให้เนื้อหานั้นมีเอฟเฟกต์แบบอนุภาคประกาย" - rotate: "หมุนหน้าจอ" - rotateDescription: "เปลี่ยนเนื้อหาตามด้วยมุมที่ระบุไว้" - plain: "เรียบง่าย" - plainDescription: "ปิดการใช้งานเอฟเฟกต์ของ MFM ทั้งหมดที่มีอยู่ในเอฟเฟกต์ MFM นี้" _instanceTicker: none: "ไม่ต้องแสดง" remote: "แสดงสำหรับผู้ใช้ระยะไกล" @@ -1519,6 +1468,7 @@ _ago: weeksAgo: "{n} สัปดาห์ที่แล้ว" monthsAgo: "{n} เดือนที่แล้ว" yearsAgo: "{n} ปีที่ผ่านมา" + invalid: "ไม่พบผลลัพธ์" _time: second: "วินาที" minute: "นาที" @@ -1552,14 +1502,29 @@ _tutorial: step8_3: "คุณสามารถเปลี่ยนการตั้งค่านี้ในภายหลังได้ตลอดเวลานะ" _2fa: alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว" - registerDevice: "ลงทะเบียนอุปกรณ์ใหม่" - registerKey: "ลงทะเบียนรหัสความปลอดภัย" + registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์" + passwordToTOTP: "กรอกรหัสผ่าน" step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ" step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้" + step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้" step2Url: "คุณยังสามารถป้อนบน URL นี้หากคุณใช้โปรแกรมเดสก์ท็อป:" + step3Title: "ป้อนรหัสยืนยัน" step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า" step4: "นับจากนี้เป็นต้นไปการพยายามเข้าสู่ระบบในอนาคตนั้น อาจจะต้องขอโทเค็นในการเข้าสู่ระบบดังกล่าว" + securityKeyNotSupported: "เบราว์เซอร์ของคุณไม่รองรับคีย์ความปลอดภัยนะ" + registerTOTPBeforeKey: "กรุณาตั้งค่าแอปยืนยันตัวตนเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" securityKeyInfo: "นอกจากนี้การตรวจสอบความถูกต้องด้วยลายนิ้วมือหรือ PIN แล้ว คุณยังสามารถตั้งค่าการตรวจสอบสิทธิ์ผ่านคีย์ความปลอดภัยของฮาร์ดแวร์ที่รองรับ FIDO2 เพื่อเพิ่มความปลอดภัยให้กับบัญชีของคุณ" + chromePasskeyNotSupported: "ขณะนี้ยังไม่รองรับรหัสผ่านของ Chrome" + registerSecurityKey: "ลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" + securityKeyName: "ป้อนชื่อคีย์" + tapSecurityKey: "กรุณาทำตามเบราว์เซอร์ของคุณเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" + removeKey: "ลบคีย์ความปลอดภัยออก" + removeKeyConfirm: "ลบข้อมูลสำรอง {name} มั้ย?" + whyTOTPOnlyRenew: "ไม่สามารถลบแอปตัวรับรองความถูกต้องได้ตราบใดที่มีการลงทะเบียนคีย์ความปลอดภัยไว้แล้ว" + renewTOTP: "กำหนดค่าแอพตัวตรวจสอบสิทธิ์ใหม่" + renewTOTPConfirm: "วิธีการแบบนี้จะทําให้รหัสยืนยันจากแอพก่อนหน้าของคุณหยุดทํางานเลยนะ" + renewTOTPOk: "ตั้งค่าคอนฟิกใหม่" + renewTOTPCancel: "ไม่เป็นไร" _permissions: "read:account": "ดูข้อมูลบัญชีของคุณ" "write:account": "แก้ไขข้อมูลบัญชีของคุณ" @@ -1594,18 +1559,20 @@ _permissions: "read:gallery-likes": "ดูรายการโพสต์ในแกลเลอรีที่ชอบของคุณ" "write:gallery-likes": "แก้ไขรายการโพสต์ในแกลเลอรีที่ชอบของคุณ" _auth: + shareAccessTitle: "การให้สิทธิ์แอปพลิเคชัน" shareAccess: "คุณต้องการอนุญาตให้ \"{name}\" เข้าถึงบัญชีนี้เลยมั้ย?" shareAccessAsk: "คุณแน่ใจแล้วจริงๆหรอว่าต้องการอนุญาตให้แอปพลิเคชันนี้เข้าถึงบัญชีของคุณแน่ใจแล้วหรอ?" + permission: "{name} ได้ขอสิทธิ์การเข้าถึงดังต่อไปนี้" permissionAsk: "แอปพลิเคชันนี้ขอสิทธิ์ดังต่อไปนี้" pleaseGoBack: "กรุณากลับไปที่แอปพลิเคชัน" callback: "กำลังกลับไปที่แอปพลิเคชัน" denied: "ปฏิเสธการเข้าใช้" + pleaseLogin: "กรุณาเข้าสู่ระบบเพื่ออนุมัติแอปพลิเคชัน" _antennaSources: all: "โน้ตทั้งหมด" homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม" users: "โน้ตจากผู้ใช้ที่เฉพาะเจาะจง" userList: "โน้ตจากรายชื่อผู้ใช้ที่ระบุ" - userGroup: "โน้ตจากผู้ใช้ในกลุ่มที่ระบุ" _weekday: sunday: "วันอาทิตย์" monday: "วันจันทร์" @@ -1680,8 +1647,8 @@ _visibility: followersDescription: "ทำให้ผู้ติดตามนั้นมองเห็นแค่คุณเท่านั้น" specified: "ไดเร็ค" specifiedDescription: "ทำให้มองเห็นได้เฉพาะผู้ใช้ที่ระบุเท่านั้น" - localOnly: "เฉพาะท้องถิ่น" - localOnlyDescription: "ผู้ใช้ระยะไกลนั้นไม่สามารถมองเห็นได้" + disableFederation: "ไม่มีสหภาพ" + disableFederationDescription: "อย่าส่งไปยังอินสแตนซ์อื่น" _postForm: replyPlaceholder: "ตอบกลับโน้ตนี้..." quotePlaceholder: "อ้างโน้ตนี้..." @@ -1819,12 +1786,9 @@ _notification: youGotReply: "{name} ตอบกลับถึงคุณ" youGotQuote: "{name} อ้างถึงคุณ" youRenoted: "รีโน้ตจาก {name}" - youGotMessagingMessageFromUser: "{name} ได้ส่งข้อความแชทถึงคุณ" - youGotMessagingMessageFromGroup: "ข้อความแชทถูกส่งไปยัง {name} กลุ่ม" youWereFollowed: "ได้ติดตามคุณ" youReceivedFollowRequest: "คุณมีคำขอติดตามใหม่น่ะ" yourFollowRequestAccepted: "คำขอติดตามของคุณได้รับการยอมรับแล้วน่ะ" - youWereInvitedToGroup: "{userName} ได้เชิญคุณเข้ากลุ่ม" pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน" unreadAntennaNote: "เสาอากาศ {name}" emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว" @@ -1840,7 +1804,7 @@ _notification: pollEnded: "โพลนี้สิ้นสุดลงแล้ว" receiveFollowRequest: "ได้รับคำขอติดตาม\n" followRequestAccepted: "ยอมรับคำขอติดตาม" - groupInvited: "ได้รับคำเชิญเข้ากลุ่ม" + achievementEarned: "ปลดล็อกความสำเร็จแล้ว" app: "การแจ้งเตือนจากแอปที่มีลิงก์" _actions: followBack: "ติดตามกลับด้วย" @@ -1873,3 +1837,6 @@ _deck: channel: "แชนแนล" mentions: "พูดถึง" direct: "ไดเร็ค" +_dialog: + charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}" + charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index ebcdfa5bfb..7bd8188a48 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -48,8 +48,6 @@ smtpUser: "Kullanıcı Adı" smtpPass: "Şifre" user: "Kullanıcı" searchByGoogle: "Arama" -_mfm: - search: "Arama" _sfx: notification: "Bildirim" _widgets: diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml index a7504542d0..65ef841259 100644 --- a/locales/ug-CN.yml +++ b/locales/ug-CN.yml @@ -2,5 +2,3 @@ _lang_: "ياپونچە" search: "ئىزدەش" searchByGoogle: "ئىزدەش" -_mfm: - search: "ئىزدەش" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 68e949f920..56e3f024a1 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -49,6 +49,7 @@ deleteAndEdit: "Видалити й редагувати" deleteAndEditConfirm: "Ви впевнені, що хочете видалити цю нотатку та відредагувати її? Ви втратите всі реакції, поширення та відповіді на неї." addToList: "Додати до списку" sendMessage: "Надіслати повідомлення" +copyRSS: "Скопіювати RSS" copyUsername: "Скопіювати ім’я користувача" searchUser: "Пошук користувачів" reply: "Відповісти" @@ -128,6 +129,7 @@ unblockConfirm: "Ви впевнені, що хочете розблокуват suspendConfirm: "Ви впевнені, що хочете призупинити цей акаунт?" unsuspendConfirm: "Ви впевнені, що хочете відновити цей акаунт?" selectList: "Виберіть список" +selectChannel: "Виберіть канал" selectAntenna: "Виберіть антену" selectWidget: "Виберіть віджет" editWidgets: "Редагувати віджети" @@ -166,7 +168,7 @@ recipient: "Отримувач" annotation: "Коментарі" federation: "Федіверс" instances: "Інстанс" -registeredAt: "Приєднався(лась)" +registeredAt: "Реєстрація" latestRequestReceivedAt: "Останній запит прийнято" latestStatus: "Останній статус" storageUsage: "Використання простору" @@ -255,6 +257,7 @@ noMoreHistory: "Подальшої історії немає" startMessaging: "Розпочати діалог" nUsersRead: "Прочитали {n}" agreeTo: "Я погоджуюсь з {0}" +agreeBelow: "Я погоджуюся з наведеним нижче" tos: "Умови використання" start: "Розпочати" home: "Домівка" @@ -263,7 +266,7 @@ activity: "Активність" images: "Зображення" birthday: "День народження" yearsOld: "{age} років" -registeredDate: "Приєднався(лась)" +registeredDate: "Приєднання" location: "Локація" theme: "Тема" themeForLightMode: "Світла тема" @@ -387,13 +390,12 @@ about: "Інформація" aboutMisskey: "Про Misskey" administrator: "Адмін" token: "Токен" -twoStepAuthentication: "Двохфакторна аутентифікація" +2fa: "Двофакторна аутентифікація" +totp: "Програма аутентифікації" moderator: "Модератор" moderation: "Модерація" nUsersMentioned: "Згадали: {n}" securityKey: "Ключ захисту" -securityKeyName: "Назва ключа" -registerSecurityKey: "Зареєструвати ключ захисту" lastUsed: "Востаннє використано" unregister: "Скасувати реєстрацію" passwordLessLogin: "Налаштувати вхід без пароля" @@ -411,24 +413,15 @@ markAsReadAllTalkMessages: "Позначити всі повідомлення help: "Допомога" inputMessageHere: "Введіть повідомлення тут" close: "Закрити" -group: "Група" -groups: "Групи" -createGroup: "Створити групу" -ownedGroups: "Власні групи" -joinedGroups: "Членство в групах" invites: "Запросити" -groupName: "Назва групи" members: "Учасники" transfer: "Передача" -messagingWithUser: "Чат з користувачами" -messagingWithGroup: "Чат з групою" title: "Тема" text: "Текст" enable: "Увімкнути" next: "Далі" retype: "Введіть ще раз" noteOf: "Нотатка {user}" -inviteToGroup: "Запрошення до групи" quoteAttached: "Цитата" quoteQuestion: "Ви хочете додати цитату?" noMessagesYet: "Ще немає повідомлень" @@ -450,18 +443,15 @@ passwordMatched: "Все вірно" passwordNotMatched: "Паролі не співпадають" signinWith: "Увійти за допомогою {x}" signinFailed: "Не вдалося увійти. Введені ім’я користувача або пароль неправильнi." -tapSecurityKey: "Торкніться ключа безпеки" or: "або" language: "Мова" uiLanguage: "Мова інтерфейсу" -groupInvited: "Запрошення до групи" aboutX: "Про {x}" disableDrawer: "Не використовувати висувні меню" -youHaveNoGroups: "Немає груп" -joinOrCreateGroup: "Отримуйте запрошення до груп або створюйте свої власні групи." noHistory: "Історія порожня" signinHistory: "Історія входів" -disableAnimatedMfm: "Відключити анімації MFM" +enableAdvancedMfm: "Увімкнути розширений MFM" +enableAnimatedMfm: "Увімкнути анімований MFM" doing: "Виконується" category: "Категорія" tags: "Теги" @@ -714,6 +704,7 @@ accentColor: "Акцент" textColor: "Текст" saveAs: "Зберегти як…" advanced: "Розширені" +advancedSettings: "Розширені налаштування" value: "Значення" createdAt: "Створено" updatedAt: "Останнє оновлення" @@ -778,6 +769,7 @@ popularPosts: "Популярні дописи" shareWithNote: "Поділитися нотаткою" ads: "Реклама" expiration: "Опитування закінчується" +startingperiod: "Початковий період" memo: "Примітка" priority: "Пріоритет" high: "Високий" @@ -826,8 +818,6 @@ deleteAccountConfirm: "Це незворотно видалить ваш ака incorrectPassword: "Неправильний пароль." voteConfirm: "Підтверджуєте свій голос за \"{choice}\"?" hide: "Сховати" -leaveGroup: "Залишити групу" -leaveGroupConfirm: "Залишити \"{name}\"?" welcomeBackWithName: "З поверненням, {name}!" clickToFinishEmailVerification: "Натисніть [{ok}], щоб завершити перевірку email." overridedDeviceKind: "Тип пристрою" @@ -841,6 +831,7 @@ searchByGoogle: "Пошук" instanceDefaultLightTheme: "Світла тема за промовчанням" instanceDefaultDarkTheme: "Темна тема за промовчанням" mutePeriod: "Тривалість приховування" +period: "Опитування закінчується" indefinitely: "Ніколи" tenMinutes: "10 хвилин" oneHour: "1 година" @@ -898,8 +889,17 @@ like: "Вподобати" unlike: "Не вподобати" numberOfLikes: "Вподобання" show: "Відображення" +roles: "Ролі" +role: "Роль" +normalUser: "Звичайний користувач" +undefined: "Не визначено" +assign: "Призначити" +unassign: "Скасувати призначення" color: "Колір" achievements: "Досягнення" +joinThisServer: "Зареєструватися на цьому сервері" +exploreOtherServers: "Знайти інший сервер" +letsLookAtTimeline: "Перегляд історії" _achievements: earnedAt: "Відкрито" _types: @@ -1087,6 +1087,9 @@ _achievements: _outputHelloWorldOnScratchpad: title: "Hello, world!" description: "Вивести \"hello world\" у Скретчпаді" + _reactWithoutRead: + title: "Прочитали як слід?" + description: "Реакція на нотатку, що містить понад 100 символів, протягом 3 секунд після її публікації" _clickedClickHere: title: "Натисніть тут" description: "Натиснуто тут" @@ -1111,11 +1114,20 @@ _achievements: _loggedInOnNewYearsDay: title: "З Новим роком!" description: "Увійшли в перший день року" + _cookieClicked: + flavor: "Чекайте, це вірний сайт?" _brainDiver: title: "Brain Diver" description: "Відправити посилання на \"Brain Diver\"" flavor: "Misskey-Misskey La-Tu-Ma" _role: + new: "Нова роль" + edit: "Змінити роль" + name: "Назва ролі" + description: "Опис ролі" + permission: "Права ролі" + assignTarget: "Призначити" + manual: "Вручну" priority: "Пріоритет" _priority: low: "Низький" @@ -1191,65 +1203,6 @@ _nsfw: respect: "Приховувати NSFW медіа" ignore: "Не приховувати NSFW медіа" force: "Приховувати всі медіа файли" -_mfm: - cheatSheet: " Довідка MFM" - intro: "MFM це ексклюзивна мова розмітки тексту в Misskey, яку можна використовувати в багатьох місцях. Тут ви можете переглянути приклади її синтаксису." - dummy: "Misskey розширює світ Федіверсу" - mention: "Згадка" - mentionDescription: "За допомогою знака \"@\" перед ім'ям можна згадати конкретного користувача." - hashtag: "Хештеґ" - hashtagDescription: "За допомогою знака \"решітка\" перед словом задається хештег." - url: "URL" - urlDescription: "Відображаються URL-адреси." - link: "Посилання" - linkDescription: "Окремі частини тексту можуть містити посилання" - bold: "Жирний шрифт" - boldDescription: "Виділяє літери, роблячи їх товще" - small: "Дрібний шрифт" - smallDescription: "Робить текст маленьким і тонким" - center: "По центру" - centerDescription: "Показує вміст у центрі" - inlineCode: "Код (у рядку)" - inlineCodeDescription: "Показує фрагмент тексту у рядку як програмний код" - blockCode: "Код (блок)" - blockCodeDescription: "Показує кілька рядків тексту як блок програмного кода" - inlineMath: "Формула (у рядку)" - inlineMathDescription: "Відображення математичних формул (KaTeX) у рядку" - blockMath: "Формули (блок)" - blockMathDescription: "Відображати багаторядкові формули (KaTeX) блоками" - quote: "Цитата" - quoteDescription: "Відображає зміст як цитату." - emoji: "Кастомні емоджі" - emojiDescription: "Щоб показати нетиповий емоджі, потрібно ввести його назву в двокрапках." - search: "Пошук" - searchDescription: "Відображає вікно пошуку з попередньо введеним текстом" - flip: "Перевернути" - flipDescription: "Віддзеркалює вміст по горизонталі або вертикалі" - jelly: "Анімація (желе)" - jellyDescription: "Створює желеподібну анімацію" - tada: "Анімація (Тада!)" - tadaDescription: "Створює анімацію з відчуттям \"Тада!\"" - jump: "Анімація (стрибки)" - jumpDescription: "Показує стрибаючу анімацію" - bounce: "Анімація (пружина)" - bounceDescription: "Надає вмісту стрибаючу анімацію." - shake: "Анімація (Shake)" - shakeDescription: "Надає вмісту тремтливу анімацію." - twitch: "Анімація (Twitch)" - spin: "Анімація (Spin)" - x2: "Великий" - x2Description: "Показує контент збільшеним." - x3: "Дуже великий" - x3Description: "Показує контент ще більшим." - x4: "Надзвичайно великий" - x4Description: "Показує контент надзвичайно великим." - blur: "Розмиття" - blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, якщо навести на нього вказівник миші." - font: "Шрифт" - fontDescription: "Встановлює шрифт для контенту." - rotate: "Обертати" - plain: "Звичайний" - plainDescription: "Деактивує всі ефекти MFM, що містяться в цьому ефекті MFM." _instanceTicker: none: "Не відображати" remote: "Відображати для віддалених користувачів" @@ -1372,6 +1325,7 @@ _ago: weeksAgo: "{n} тиж. тому" monthsAgo: "{n} міс. тому" yearsAgo: "{n} р. тому" + invalid: "Тут нічого немає" _time: second: "с" minute: "х" @@ -1404,13 +1358,12 @@ _tutorial: step8_3: "Ви завжди можете змінити цей параметр пізніше." _2fa: alreadyRegistered: "Двофакторна автентифікація вже налаштована." - registerDevice: "Зареєструвати новий пристрій" - registerKey: "Зареєструвати новий ключ безпеки" step1: "Спершу встановіть на свій пристрій програму автентифікації (наприклад {a} або {b})." step2: "Потім відскануйте QR-код, який відображається на цьому екрані." step2Url: "Ви також можете ввести цю URL-адресу, якщо використовуєте програму для ПК:" step3: "Щоб завершити налаштування, введіть токен, наданий вашою програмою." step4: "Відтепер будь-які майбутні спроби входу вимагатимуть такого токена." + renewTOTPCancel: "Не зараз" _permissions: "read:account": "Переглядати дані профілю" "write:account": "Змінити дані акаунту" @@ -1519,8 +1472,6 @@ _visibility: followersDescription: "Тільки для підписників" specified: "Особисто" specifiedDescription: "Лише для певних користувачів" - localOnly: "Локально" - localOnlyDescription: "Приховано для віддалених користувачів" _postForm: replyPlaceholder: "Відповідь на цю нотатку..." quotePlaceholder: "Прокоментуйте цю нотатку..." @@ -1645,12 +1596,9 @@ _notification: youGotReply: "{name} відповідає" youGotQuote: "{name} цитує вас" youRenoted: "{name} поширює" - youGotMessagingMessageFromUser: "Повідомлення від {name}" - youGotMessagingMessageFromGroup: "Нове повідомлення в групі {name}" youWereFollowed: "Новий підписник" youReceivedFollowRequest: "Ви отримали запит на підписку" yourFollowRequestAccepted: "Запит на підписку прийнято" - youWereInvitedToGroup: "Запрошення до групи" achievementEarned: "Досягнення відкрито" _types: all: "Все" @@ -1662,7 +1610,6 @@ _notification: reaction: "Реакції" receiveFollowRequest: "Запити на підписку" followRequestAccepted: "Прийняті підписки" - groupInvited: "Запрошення до груп" app: "Сповіщення від додатків" _actions: reply: "Відповісти" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 26527c74c3..ce36de03db 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -2,6 +2,7 @@ _lang_: "Tiếng Việt" headlineMisskey: "Mạng xã hội liên hợp" introMisskey: "Xin chào! Misskey là một nền tảng tiểu blog phi tập trung mã nguồn mở.\nViết \"tút\" để chia sẻ những suy nghĩ của bạn 📡\nBằng \"biểu cảm\", bạn có thể bày tỏ nhanh chóng cảm xúc của bạn với các tút 👍\nHãy khám phá một thế giới mới! 🚀" +poweredByMisskeyDescription: "{name} là một trong những chủ máy của Misskey là nền tảng mã nguồn mở" monthAndDay: "{day} tháng {month}" search: "Tìm kiếm" notifications: "Thông báo" @@ -10,12 +11,13 @@ password: "Mật khẩu" forgotPassword: "Quên mật khẩu" fetchingAsApObject: "Đang nạp dữ liệu từ Fediverse..." ok: "Đồng ý" -gotIt: "Đã hiểu!" +gotIt: "Hiểu rồi!" cancel: "Hủy" +noThankYou: "Không, cảm ơn" enterUsername: "Nhập tên người dùng" renotedBy: "Chia sẻ bởi {user}" -noNotes: "Chưa có tút nào." -noNotifications: "Không có thông báo" +noNotes: "Chưa có bài viết nào." +noNotifications: "Chưa có thông báo" instance: "Máy chủ" settings: "Cài đặt" basicSettings: "Thiết lập chung" @@ -47,6 +49,7 @@ deleteAndEdit: "Sửa" deleteAndEditConfirm: "Bạn có chắc muốn sửa tút này? Những biểu cảm, lượt trả lời và đăng lại sẽ bị mất." addToList: "Thêm vào danh sách" sendMessage: "Gửi tin nhắn" +copyRSS: "Sao chép RSS" copyUsername: "Chép tên người dùng" searchUser: "Tìm kiếm người dùng" reply: "Trả lời" @@ -65,13 +68,13 @@ export: "Xuất dữ liệu" files: "Tập tin" download: "Tải xuống" driveFileDeleteConfirm: "Bạn có chắc muốn xóa tập tin \"{name}\"? Tút liên quan cũng sẽ bị xóa theo." -unfollowConfirm: "Bạn có chắc muốn ngưng theo dõi {name}?" +unfollowConfirm: "Bạn ngừng theo dõi {name}?" exportRequested: "Đang chuẩn bị xuất tập tin. Quá trình này có thể mất ít phút. Nó sẽ được tự động thêm vào Drive sau khi hoàn thành." importRequested: "Bạn vừa yêu cầu nhập dữ liệu. Quá trình này có thể mất ít phút." lists: "Danh sách" noLists: "Bạn chưa có danh sách nào" -note: "Tút" -notes: "Tút" +note: "Bài viết" +notes: "Bài Viết" following: "Đang theo dõi" followers: "Người theo dõi" followsYou: "Theo dõi bạn" @@ -88,7 +91,7 @@ enterListName: "Đặt tên cho danh sách" privacy: "Bảo mật" makeFollowManuallyApprove: "Yêu cầu theo dõi cần được duyệt" defaultNoteVisibility: "Kiểu tút mặc định" -follow: "Đang theo dõi" +follow: "Theo dõi" followRequest: "Gửi yêu cầu theo dõi" followRequests: "Yêu cầu theo dõi" unfollow: "Ngưng theo dõi" @@ -100,7 +103,9 @@ renoted: "Đã đăng lại." cantRenote: "Không thể đăng lại tút này." cantReRenote: "Không thể đăng lại một tút đăng lại." quote: "Trích dẫn" -pinnedNote: "Tút ghim" +inChannelRenote: "Chia sẻ trong kênh này" +inChannelQuote: "Trích dẫn trong kênh này" +pinnedNote: "Bài viết đã ghim" pinned: "Ghim" you: "Bạn" clickToShow: "Nhấn để xem" @@ -126,6 +131,7 @@ unblockConfirm: "Bạn có chắc muốn bỏ chặn người này?" suspendConfirm: "Bạn có chắc muốn vô hiệu hóa người này?" unsuspendConfirm: "Bạn có chắc muốn bỏ vô hiệu hóa người này?" selectList: "Chọn danh sách" +selectChannel: "Lựa chọn kênh" selectAntenna: "Chọn một antenna" selectWidget: "Chọn tiện ích" editWidgets: "Sửa tiện ích" @@ -141,8 +147,8 @@ cacheRemoteFiles: "Tập tin cache từ xa" cacheRemoteFilesDescription: "Khi tùy chọn này bị tắt, các tập tin từ xa sẽ được tải trực tiếp từ máy chủ khác. Điều này sẽ giúp giảm dung lượng lưu trữ nhưng lại tăng lưu lượng truy cập, vì hình thu nhỏ sẽ không được tạo." flagAsBot: "Đánh dấu đây là tài khoản bot" flagAsBotDescription: "Bật tùy chọn này nếu tài khoản này được kiểm soát bởi một chương trình. Nếu được bật, nó sẽ được đánh dấu để các nhà phát triển khác ngăn chặn chuỗi tương tác vô tận với các bot khác và điều chỉnh hệ thống nội bộ của Misskey để coi tài khoản này như một bot." -flagAsCat: "Tài khoản này là mèo" -flagAsCatDescription: "Bật tùy chọn này để đánh dấu tài khoản là một con mèo." +flagAsCat: "Chế độ Mèeeeeeeeeeo!!" +flagAsCatDescription: "Nếu mà em là một con mèo thì cứ bật nó kiu mèo mèo mèeeeeeeo!! " flagShowTimelineReplies: "Hiện lượt trả lời trong bảng tin" flagShowTimelineRepliesDescription: "Hiện lượt trả lời của người bạn theo dõi trên tút của những người khác." autoAcceptFollowed: "Tự động phê duyệt theo dõi từ những người mà bạn đang theo dõi" @@ -155,7 +161,7 @@ setWallpaper: "Đặt ảnh bìa" removeWallpaper: "Xóa ảnh bìa" searchWith: "Tìm kiếm: {q}" youHaveNoLists: "Bạn chưa có danh sách nào" -followConfirm: "Bạn có chắc muốn theo dõi {name}?" +followConfirm: "Bạn theo dõi {name}?" proxyAccount: "Tài khoản proxy" proxyAccountDescription: "Tài khoản proxy là tài khoản hoạt động như một người theo dõi từ xa cho người dùng trong những điều kiện nhất định. Ví dụ: khi người dùng thêm người dùng từ xa vào danh sách, hoạt động của người dùng từ xa sẽ không được chuyển đến phiên bản nếu không có người dùng cục bộ nào theo dõi người dùng đó, vì vậy tài khoản proxy sẽ theo dõi." host: "Host" @@ -198,7 +204,7 @@ blockedUsers: "Người đã chặn" noUsers: "Chưa có ai" editProfile: "Sửa hồ sơ" noteDeleteConfirm: "Bạn có chắc muốn xóa tút này?" -pinLimitExceeded: "Bạn đã đạt giới hạn số lượng tút có thể ghim" +pinLimitExceeded: "Bạn không thể ghim bài viết nữa" intro: "Đã cài đặt Misskey! Xin hãy tạo tài khoản admin." done: "Xong" processing: "Đang xử lý" @@ -253,6 +259,8 @@ noMoreHistory: "Không còn gì để đọc" startMessaging: "Bắt đầu trò chuyện" nUsersRead: "đọc bởi {n}" agreeTo: "Tôi đồng ý {0}" +agreeBelow: "Đồng ý với nội dung dưới đây" +basicNotesBeforeCreateAccount: "Những điều cơ bản cần chú ý " tos: "Điều khoản dịch vụ" start: "Bắt đầu" home: "Trang chính" @@ -339,7 +347,7 @@ pinnedUsersDescription: "Liệt kê mỗi hàng một tên người dùng xuốn pinnedPages: "Trang đã ghim" pinnedPagesDescription: "Liệt kê các trang thú vị để ghim trên máy chủ." pinnedClipId: "ID của clip muốn ghim" -pinnedNotes: "Tút ghim" +pinnedNotes: "Bài viết đã ghim" hcaptcha: "hCaptcha" enableHcaptcha: "Bật hCaptcha" hcaptchaSiteKey: "Khóa của trang" @@ -348,6 +356,8 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "Bật reCAPTCHA" recaptchaSiteKey: "Khóa của trang" recaptchaSecretKey: "Khóa bí mật" +turnstile: "Turnstile" +enableTurnstile: "Áp dụng Turnstile" turnstileSiteKey: "Khóa của trang" turnstileSecretKey: "Khóa bí mật" avoidMultiCaptchaConfirm: "Dùng nhiều hệ thống Captcha có thể gây nhiễu giữa chúng. Bạn có muốn tắt các hệ thống Captcha khác hiện đang hoạt động không? Nếu bạn muốn chúng tiếp tục được bật, hãy nhấn hủy." @@ -383,16 +393,19 @@ about: "Giới thiệu" aboutMisskey: "Về Misskey" administrator: "Quản trị viên" token: "Token" -twoStepAuthentication: "Xác minh 2 bước" +2fa: "Xác thực 2 yếu tố" +totp: "Ứng dụng xác thực" +totpDescription: "Nhắn mã OTP bằng ứng dụng xác thực" moderator: "Kiểm duyệt viên" moderation: "Kiểm duyệt" nUsersMentioned: "Dùng bởi {n} người" +securityKeyAndPasskey: "Mã bảo mật・Passkey" securityKey: "Khóa bảo mật" -securityKeyName: "Tên khoá" -registerSecurityKey: "Đăng ký khóa bảo mật" lastUsed: "Dùng lần cuối" +lastUsedAt: "Lần cuối sử dụng: {t}" unregister: "Hủy đăng ký" passwordLessLogin: "Đăng nhập không mật khẩu" +passwordLessLoginDescription: "Đăng nhập bằng chỉ mã bảo mật hoặc passkey, không sử dụng mật khẩu." resetPassword: "Đặt lại mật khẩu" newPasswordIs: "Mật khẩu mới là \"{password}\"" reduceUiAnimation: "Giảm chuyển động UI" @@ -407,24 +420,15 @@ markAsReadAllTalkMessages: "Đánh dấu tất cả các tin nhắn là đã đ help: "Trợ giúp" inputMessageHere: "Nhập nội dung tin nhắn" close: "Đóng" -group: "Nhóm" -groups: "Các nhóm" -createGroup: "Tạo nhóm" -ownedGroups: "Nhóm tôi quản lý" -joinedGroups: "Nhóm tôi tham gia" invites: "Mời" -groupName: "Tên nhóm" members: "Thành viên" transfer: "Chuyển giao" -messagingWithUser: "Nhắn riêng" -messagingWithGroup: "Chat nhóm" title: "Tựa đề" text: "Nội dung" enable: "Bật" next: "Kế tiếp" retype: "Nhập lại" noteOf: "Tút của {user}" -inviteToGroup: "Mời vào nhóm" quoteAttached: "Trích dẫn" quoteQuestion: "Trích dẫn lại?" noMessagesYet: "Chưa có tin nhắn" @@ -435,7 +439,7 @@ invitations: "Mời" invitationCode: "Mã mời" checking: "Đang kiểm tra..." available: "Khả dụng" -unavailable: "Không khả dụng" +unavailable: "Không sử dụng được" usernameInvalidFormat: "Bạn có thể dùng viết hoa/viết thường, chữ số, và dấu gạch dưới." tooShort: "Quá ngắn" tooLong: "Quá dài" @@ -446,18 +450,17 @@ passwordMatched: "Trùng khớp" passwordNotMatched: "Không trùng khớp" signinWith: "Đăng nhập bằng {x}" signinFailed: "Không thể đăng nhập. Vui lòng kiểm tra tên người dùng và mật khẩu của bạn." -tapSecurityKey: "Nhấn mã bảo mật của bạn" or: "Hoặc" language: "Ngôn ngữ" uiLanguage: "Ngôn ngữ giao diện" -groupInvited: "Bạn đã được mời tham gia nhóm" aboutX: "Giới thiệu {x}" +emojiStyle: "Kiểu cách Emoji" +native: "Bản xứ" disableDrawer: "Không dùng menu thanh bên" -youHaveNoGroups: "Không có nhóm nào" -joinOrCreateGroup: "Tham gia hoặc tạo một nhóm mới." noHistory: "Không có dữ liệu" signinHistory: "Lịch sử đăng nhập" -disableAnimatedMfm: "Tắt MFM với chuyển động" +enableAdvancedMfm: "Xem bài MFM chất lượng cao." +enableAnimatedMfm: "Xem bài MFM có chuyển động" doing: "Đang xử lý..." category: "Phân loại" tags: "Thẻ" @@ -645,7 +648,7 @@ random: "Ngẫu nhiên" system: "Hệ thống" switchUi: "Chuyển đổi giao diện người dùng" desktop: "Desktop" -clip: "Ghim" +clip: "Lưu bài viết" createNew: "Tạo mới" optional: "Không bắt buộc" createNewClip: "Tạo một ghim mới" @@ -684,7 +687,7 @@ pageLikesCount: "Số lượng trang đã thích" pageLikedCount: "Số lượng thích trang đã nhận" contact: "Liên hệ" useSystemFont: "Dùng phông chữ mặc định của hệ thống" -clips: "Ghim" +clips: "Lưu bài viết" experimentalFeatures: "Tính năng thử nghiệm" developer: "Nhà phát triển" makeExplorable: "Không hiện tôi trong \"Khám phá\"" @@ -710,6 +713,7 @@ accentColor: "Màu phụ" textColor: "Màu chữ" saveAs: "Lưu thành" advanced: "Nâng cao" +advancedSettings: "Cài đặt nâng cao" value: "Giá trị" createdAt: "Ngày tạo" updatedAt: "Cập nhật lúc" @@ -775,6 +779,7 @@ popularPosts: "Tút được xem nhiều nhất" shareWithNote: "Chia sẻ kèm với tút" ads: "Quảng cáo" expiration: "Thời hạn" +startingperiod: "Thời gian bắt đầu\n" memo: "Lưu ý" priority: "Ưu tiên" high: "Cao" @@ -807,6 +812,7 @@ lastCommunication: "Lần giao tiếp cuối" resolved: "Đã xử lý" unresolved: "Chờ xử lý" breakFollow: "Xóa người theo dõi" +breakFollowConfirm: "Bạn bỏ theo dõi tài khoản này không?" itsOn: "Đã bật" itsOff: "Đã tắt" emailRequiredForSignup: "Yêu cầu địa chỉ email khi đăng ký" @@ -826,8 +832,6 @@ deleteAccountConfirm: "Điều này sẽ khiến tài khoản bị xóa vĩnh vi incorrectPassword: "Sai mật khẩu." voteConfirm: "Xác nhận bình chọn \"{choice}\"?" hide: "Ẩn" -leaveGroup: "Rời khỏi nhóm" -leaveGroupConfirm: "Bạn có chắc muốn rời khỏi nhóm \"{name}\"?" useDrawerReactionPickerForMobile: "Hiện bộ chọn biểu cảm dạng xổ ra trên điện thoại" welcomeBackWithName: "Chào mừng trở lại, {name}" clickToFinishEmailVerification: "Vui lòng nhấn [{ok}] để hoàn tất việc đăng ký." @@ -843,16 +847,19 @@ instanceDefaultLightTheme: "Theme máy chủ Sáng-Rộng" instanceDefaultDarkTheme: "Theme máy chủ Tối-Rộng" instanceDefaultThemeDescription: "Nhập mã theme trong định dạng đối tượng." mutePeriod: "Thời hạn ẩn" +period: "Thời hạn" indefinitely: "Vĩnh viễn" tenMinutes: "10 phút" oneHour: "1 giờ" oneDay: "1 ngày" oneWeek: "1 tuần" +oneMonth: "1 tháng" reflectMayTakeTime: "Có thể mất một thời gian để điều này được áp dụng." failedToFetchAccountInformation: "Không thể lấy thông tin tài khoản" rateLimitExceeded: "Giới hạn quá mức" cropImage: "Cắt hình ảnh" cropImageAsk: "Bạn có muốn cắt ảnh này?" +cropNo: "Để nguyên" file: "Tập tin" recentNHours: "{n}h trước" recentNDays: "{n} ngày trước" @@ -895,15 +902,231 @@ navbar: "Thanh điều hướng" shuffle: "Xáo trộn" account: "Tài khoản của bạn" move: "Di chuyển" +pushNotification: "Thông báo đẩy" +subscribePushNotification: "Bật thông báo đẩy" +unsubscribePushNotification: "Tắt thông báo đẩy" +pushNotificationAlreadySubscribed: "Đang bật thông báo đẩy" +sendPushNotificationReadMessage: "Xóa thông báo đẩy sau khi đọc thông báo hay tin nhắn" +sendPushNotificationReadMessageCaption: "Thông báo như {emptyPushNotificationMessage} sẽ hiển thị trong giây phút. Tiêu tốn pin của máy bạn có thể tăng lên hơn nữa." +windowMaximize: "Phóng to" +windowRestore: "Khôi phục" +caption: "Mô tả" +loggedInAsBot: "Đang đăng nhập bằng tài khoản Bot" +tools: "Công Cụ" +cannotLoad: "Không tải được" +numberOfProfileView: "Số lần mở hồ sơ" like: "Thích" +unlike: "Bỏ lượt thích" +numberOfLikes: "Lượt thích" show: "Hiển thị" +neverShow: "Không hiển thị nữa" +remindMeLater: "Để sau" +didYouLikeMisskey: "Bạn có ưa thích Mískey không?" +pleaseDonate: "Misskey là phần mềm miễn phí mà {host} đang sử dụng. Xin mong bạn quyên góp cho chúng tôi để chúng tôi có thể tiếp tục phát triển dịch vụ này. Xin cảm ơn!!" +roles: "Vai trò" +role: "Vai trò" +normalUser: "Người dùng bình thường" +undefined: "Chưa xác định" color: "Màu sắc" +manageCustomEmojis: "Quản lý CustomEmoji" +cannotPerformTemporary: "Tạm thời không sử dụng được" +cannotPerformTemporaryDescription: "Tạm thời không sử dụng được vì lần số điều kiện quá giới hạn. Thử lại sau mọt lát nữa." +achievements: "Thành tích" +gotInvalidResponseError: "Không nhận được trả lời chủ máy" +gotInvalidResponseErrorDescription: "Chủ máy có lẻ ngừng hoạt động hoặc bảo trí. Thử lại sau một lát nữa. " +thisPostMayBeAnnoying: "Bạn đăng bài này có thể làm phiền cho người ta." +thisPostMayBeAnnoyingHome: "Đăng trên trang chính" +thisPostMayBeAnnoyingCancel: "Từ chối" +thisPostMayBeAnnoyingIgnore: "Đăng bài để nguyên" +collapseRenotes: "Không hiển thị bài viết đã từng xem" +internalServerError: "Lỗi trong chủ máy" +internalServerErrorDescription: "Trong chủ máy lỗi bất ngờ xảy ra" +copyErrorInfo: "Sao chép thông tin lỗi" +joinThisServer: "Đăng ký trên chủ máy này" +exploreOtherServers: "Tìm chủ máy khác" +letsLookAtTimeline: "Thử xem Timeline" +_achievements: + earnedAt: "Ngày thu nhận" + _types: + _notes1: + title: "just setting up my msky" + description: "Lần đầu tiên đăng bài" + flavor: "Chúc bạn trên Miskey vui vẻ nha!!" + _notes10: + title: "Một số bài viết" + description: "Đăng bài 10 lần" + _notes100: + title: "Rất nhiều bài biết" + description: "Đăng bài 100 lần" + _notes500: + title: "Như đầy bài viết" + description: "Đăng bài 500 lần" + _notes1000: + title: "Ngọn núi bài viết" + description: "Đăng bài 1000 lần" + _notes5000: + title: "Bài viết chảy như suối" + description: "Đăng bài 5000 lần" + _notes10000: + title: "Bài Viết siu nhìu" + description: "Đăng bài 10000 lần" + _notes20000: + title: "Need more note" + description: "Đã đăng bài 20,000 lần rồi" + _notes30000: + title: "ĐĂNG VỚI BÀI" + description: "Đã đăng bài 30,000 lần rồi" + _notes40000: + title: "Nhà xưởng dăng bài" + description: "Đã đăng bài 40,000 lần rồi" + _notes50000: + title: "Hàng tinh đăng bài" + description: "Đã đăng bài 50,000 lần rồi" + _notes100000: + flavor: "Liệu viết bài gì tầm này vậy? " + _login3: + title: "Sơ cấp I" + description: "Tổng số ngày đăng nhập đạt 3 ngày" + flavor: "Từ nay các bạn cứ xem như mình là một Misskist đó" + _login7: + title: "Sơ cấp II" + description: "Tổng số ngày đăng nhập đạt 7 ngày" + flavor: "Bạn dần quen chưa? " + _login15: + title: "Sơ cấp III" + description: "Tổng số ngày đăng nhập đạt 7 ngày" + _login30: + title: "Misskist cấp I" + description: "Tổng số ngày đăng nhập đạt 30 ngày" + _login60: + title: "Misskist cấp II" + description: "Tổng số ngày đăng nhập đạt 60 ngày" + _login100: + title: "Misskist cấp III" + description: "Tổng số ngày đăng nhập đạt 100 ngày" + flavor: "Người dùng này, chính vì đó là một Misskist" + _login200: + title: "Khách hàng thường xuyên cấp I" + description: "Tổng số ngày đăng nhập đạt 200 ngày" + _login300: + title: "Khách hàng thường xuyên cấp II" + description: "Tổng số ngày đăng nhập đạt 300 ngày" + _login400: + title: "Khách hàng thường xuyên cấp III" + description: "Tổng số ngày đăng nhập đạt 400 ngày" + _markedAsCat: + title: "Tôi là một con mèo" + description: "Bật chế độ mèo" + flavor: "Mà tên chưa có" + _following1: + title: "Theo dõi đầu tiên" + description: "Lần đầu tiên theo dõi " + _following10: + title: "Cứ theo dõi và theo dõi" + description: "Vừa theo dõi hơn 10 người" + _following50: + title: "Bạn bè nhiều quá" + description: "Vừa theo dõi hơn 50 người" + _following100: + title: "Trăm bạn bè" + description: "Vừa theo dõi vượt lên 100 người" + _following300: + title: "Quá nhiều bạn bè" + description: "Vừa theo dõi vượt lên 300 người" + _followers1: + title: "Ai đầu tiên theo dõi bạn" + description: "Lần đầu tiên được theo dõi" + _followers10: + title: "FOLLOW ME!!" + description: "Người theo dõi bạn vượt lên 10 người" + _followers500: + title: "Trạm phát sóng" + _followers1000: + title: "Người có tầm ảnh hưởng" + description: "Người theo dõi bạn vượt lên 1000 người" + _collectAchievements30: + title: "Người sưu tập thành tích" + description: "Vừa lấy thành tích hơn 30 cái" + _viewAchievements3min: + title: "Yêu Thành tích" + description: "Ngắm danh sách thành tích đến tận hơn 3 phút" + _iLoveMisskey: + title: "Tôi Yêu Misskey" + description: "Đăng lời nói \"I ❤ #Misskey\"" + flavor: "Xin chân thành cảm ơn bạn đã sử dụng Misskey!! by Đội ngũ phát triển" + _foundTreasure: + title: "Tìm kiếm kho báu" + description: "Tìm thấy được những kho báu cất giấu" + _client30min: + title: "Giải lao xỉu" + _noteDeletedWithin1min: + title: "Xem như không có gì đâu nha" + _postedAtLateNight: + title: "Loài ăn đêm" + description: "Đăng bài trong đêm khuya " + _postedAt0min0sec: + title: "Tín hiệu báo giờ" + description: "Đăng bài vào 0 phút 0 giây" + flavor: "Piiiiiii ĐÂY LÀ TIẾNG NÓI VIỆT NAM" + _selfQuote: + title: "Nói đến bản thân" + description: "Trích dẫn bài viết của mình" + _htl20npm: + title: "Timeline trôi như con sông" + description: "Timeline trang chính tốc độ vượt lên 20npm" + _viewInstanceChart: + title: "Nhà phân tích" + description: "Xem biểu đồ của chủ máy" + _outputHelloWorldOnScratchpad: + title: "Chào thế giới!" + _open3windows: + title: "Nhiều cửa sổ" + description: "Mở cửa sổ hơn 3 cửa sổ" + _reactWithoutRead: + title: "Bài này bạn đọc kỹ chứ? " + description: "Phản hồi trong vọng 3 giây sau bài viết có hơn 100 ký tự mới được đăng lên" + _clickedClickHere: + title: "Bấm đây" + description: "Bấm chỗ này" + _justPlainLucky: + title: "Chỉ là một cuộc máy mắn" + description: "Mỗi 10 giây thu nhận được với tỷ lệ 0.005%" + _setNameToSyuilo: + title: "Ngưỡng mộ với vị thần" + description: "Đạt tên là syuilo" + _loggedInOnBirthday: + title: "Sinh nhật vủi vẻ" + description: "Đăng nhập vào ngày sinh" + _loggedInOnNewYearsDay: + title: "Chức mừng năm mới" + description: "Đăng nhập vào Tết Nguyên đàn dương lịch" + flavor: "Chúc bạn năm mới AN KHANG THỊNH VƯỢNG, VẠN SỰ NHƯ Ý!!" + _cookieClicked: + flavor: "Bạn nhầm phầm mềm chứ?" _role: priority: "Ưu tiên" _priority: low: "Thấp" middle: "Vừa" high: "Cao" + _options: + gtlAvailable: "Xem Timeline xã hội" + ltlAvailable: "Xem Timeline trong máy chủ này" + canPublicNote: "Cho phép đăng bài công khai" + canManageCustomEmojis: "Quản lý CustomEmoji" + driveCapacity: "Dữ liệu Drive" + pinMax: "Giới hạn ghim bài viết" + antennaMax: "Giới hạn tạo ăng ten" + canHideAds: "Tắt quảng cáo" + _condition: + createdMoreThan: "Trôi qua ~ sau khi lập tài khoản" + followersLessThanOrEq: "Người theo dõi ít hơn ~" + followersMoreThanOrEq: "Người theo dõi có ~ trở lên" + followingLessThanOrEq: "Theo dõi ít hơn ~" + followingMoreThanOrEq: "Theo dõi có ~ trở lên" + and: "~ mà ~" + or: "~ hay là ~" + not: "Không phải ~" _sensitiveMediaDetection: description: "Giảm nỗ lực kiểm duyệt máy chủ thông qua việc tự động nhận dạng media NSFW thông qua học máy. Điều này sẽ làm tăng một chút áp lực trên máy chủ." sensitivity: "Phát hiện nhạy cảm" @@ -992,72 +1215,6 @@ _nsfw: respect: "Ẩn nội dung NSFW" ignore: "Hiện nội dung NSFW" force: "Ẩn mọi media" -_mfm: - cheatSheet: "MFM Cheatsheet" - intro: "MFM là ngôn ngữ phát triển độc quyền của Misskey có thể được sử dụng ở nhiều nơi. Tại đây bạn có thể xem danh sách tất cả các cú pháp MFM có sẵn." - dummy: "Misskey mở rộng thế giới Fediverse" - mention: "Nhắc đến" - mentionDescription: "Bạn có thể nhắc đến ai đó bằng cách sử dụng @tên người dùng." - hashtag: "Hashtag" - hashtagDescription: "Bạn có thể tạo một hashtag bằng #chữ hoặc #số." - url: "URL" - urlDescription: "Những URL có thể hiển thị." - link: "Đường dẫn" - linkDescription: "Các phần cụ thể của văn bản có thể được hiển thị dưới dạng URL." - bold: "In đậm" - boldDescription: "Nổi bật các chữ cái bằng cách làm chúng dày hơn." - small: "Nhỏ" - smallDescription: "Hiển thị nội dung nhỏ và mỏng." - center: "Giữa" - centerDescription: "Hiển thị nội dung căn giữa." - inlineCode: "Mã (Trong dòng)" - inlineCodeDescription: "Hiển thị tô sáng cú pháp trong dòng cho mã (chương trình)." - blockCode: "Mã (Khối)" - blockCodeDescription: "Hiển thị tô sáng cú pháp cho mã nhiều dòng (chương trình) trong một khối." - inlineMath: "Toán học (Trong dòng)" - inlineMathDescription: "Hiển thị công thức toán (KaTeX) trong dòng" - blockMath: "Toán học (Khối)" - blockMathDescription: "Hiển thị công thức toán học nhiều dòng (KaTeX) trong một khối" - quote: "Trích dẫn" - quoteDescription: "Hiển thị nội dung dạng lời trích dạng." - emoji: "Tùy chỉnh emoji" - emojiDescription: "Hiển thị emoji với cú pháp :tên emoji:" - search: "Tìm kiếm" - searchDescription: "Hiển thị hộp tìm kiếm với văn bản được nhập trước." - flip: "Lật" - flipDescription: "Lật nội dung theo chiều ngang hoặc chiều dọc." - jelly: "Chuyển động (Thạch rau câu)" - jellyDescription: "Cho phép nội dung chuyển động giống như thạch rau câu." - tada: "Chuyển động (Tada)" - tadaDescription: "Cho phép nội dung chuyển động kiểu \"Tada!\"." - jump: "Chuyển động (Nhảy múa)" - jumpDescription: "Cho phép nội dung chuyển động nhảy nhót." - bounce: "Chuyển động (Cà tưng)" - bounceDescription: "Cho phép nội dung chuyển động cà tưng." - shake: "Chuyển động (Rung)" - shakeDescription: "Cho phép nội dung chuyển động rung lắc." - twitch: "Chuyển động (Co rút)" - twitchDescription: "Cho phép nội dung chuyển động co rút." - spin: "Chuyển động (Xoay tít)" - spinDescription: "Cho phép nội dung chuyển động xoay tít." - x2: "Lớn" - x2Description: "Hiển thị nội dung cỡ lớn hơn." - x3: "Rất lớn" - x3Description: "Hiển thị nội dung cỡ lớn hơn nữa." - x4: "Khổng lồ" - x4Description: "Hiển thị nội dung cỡ khổng lồ." - blur: "Làm mờ" - blurDescription: "Làm mờ nội dung. Nó sẽ được hiển thị rõ ràng khi di chuột qua." - font: "Phông chữ" - fontDescription: "Chọn phông chữ để hiển thị nội dung." - rainbow: "Cầu vồng" - rainbowDescription: "Làm cho nội dung hiển thị với màu sắc cầu vồng." - sparkle: "Lấp lánh" - sparkleDescription: "Làm cho nội dung hiệu ứng hạt lấp lánh." - rotate: "Xoay" - rotateDescription: "Xoay nội dung theo một góc cụ thể." - plain: "Đơn giản" - plainDescription: "Vô hiệu hóa mọi hiệu ứng MFM chứa trong hiệu ứng MFM này." _instanceTicker: none: "Không hiển thị" remote: "Hiện cho người dùng từ máy chủ khác" @@ -1187,6 +1344,7 @@ _ago: weeksAgo: "{n} tuần trước" monthsAgo: "{n} tháng trước" yearsAgo: "{n} năm trước" + invalid: "Không có gì ở đây" _time: second: "s" minute: "phút" @@ -1215,16 +1373,23 @@ _tutorial: step7_1: "Xin chúc mừng! Bây giờ bạn đã hoàn thành phần hướng dẫn cơ bản của Misskey." step7_2: "Nếu bạn muốn tìm hiểu thêm về Misskey, hãy thử phần {help}." step7_3: "Bây giờ, chúc may mắn và vui vẻ với Misskey! 🚀" + step8_1: "Cuối cùng, bạn hãy bật thông báo đẩy nha!" + step8_2: "Nhận thông báo đẩy bạn sẽ có thể thấy phản hồi, theo dõi, lượt nhắc được trong khi đóng Misskey" _2fa: alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước." - registerDevice: "Đăng ký một thiết bị" - registerKey: "Đăng ký một mã bảo vệ" + passwordToTOTP: "Nhắn mật mã" step1: "Trước tiên, hãy cài đặt một ứng dụng xác minh (chẳng hạn như {a} hoặc {b}) trên thiết bị của bạn." step2: "Sau đó, quét mã QR hiển thị trên màn hình này." step2Url: "Bạn cũng có thể nhập URL này nếu sử dụng một chương trình máy tính:" step3: "Nhập mã token do ứng dụng của bạn cung cấp để hoàn tất thiết lập." step4: "Kể từ bây giờ, những lần đăng nhập trong tương lai sẽ yêu cầu mã token đăng nhập đó." securityKeyInfo: "Bên cạnh xác minh bằng vân tay hoặc mã PIN, bạn cũng có thể thiết lập xác minh thông qua khóa bảo mật phần cứng hỗ trợ FIDO2 để bảo mật hơn nữa cho tài khoản của mình." + removeKey: "Xóa mã bảo mật" + removeKeyConfirm: "Xóa bản sao lưu {name}?" + renewTOTP: "Cài đặt lại ứng dụng xác thực" + renewTOTPConfirm: "Mã xác nhận cũ của ứng dụng xác thực không thể sử dụng được nữa" + renewTOTPOk: "Cài đặt lại" + renewTOTPCancel: "Không, cảm ơn" _permissions: "read:account": "Xem thông tin tài khoản của bạn" "write:account": "Sửa thông tin tài khoản của bạn" @@ -1259,18 +1424,20 @@ _permissions: "read:gallery-likes": "Xem danh sách các tút đã thích trong thư viện của tôi" "write:gallery-likes": "Sửa danh sách các tút đã thích trong thư viện của tôi" _auth: + shareAccessTitle: "Cho phép truy cập app" shareAccess: "Bạn có muốn cho phép \"{name}\" truy cập vào tài khoản này không?" shareAccessAsk: "Bạn có chắc muốn cho phép ứng dụng này truy cập vào tài khoản của mình không?" + permission: "{name} đang yêu cầu quyền hạn dưới đây" permissionAsk: "Ứng dụng này yêu cầu các quyền sau" pleaseGoBack: "Vui lòng quay lại ứng dụng" callback: "Quay lại ứng dụng" denied: "Truy cập bị từ chối" + pleaseLogin: "Bạn phải đăng nhập để cho ứng dụng phép truy cập" _antennaSources: all: "Toàn bộ tút" homeTimeline: "Tút từ những người đã theo dõi" users: "Tút từ những người cụ thể" userList: "Tút từ danh sách người dùng cụ thể" - userGroup: "Tút từ người dùng trong một nhóm cụ thể" _weekday: sunday: "Chủ Nhật" monday: "Thứ Hai" @@ -1303,9 +1470,12 @@ _widgets: jobQueue: "Công việc chờ xử lý" serverMetric: "Thống kê máy chủ" aiscript: "AiScript console" + aiscriptApp: "AiScript App" aichan: "Ai" + userList: "Danh sách người dùng" _userList: chooseList: "Chọn danh sách" + clicker: "clicker" _cw: hide: "Ẩn" show: "Tải thêm" @@ -1342,8 +1512,8 @@ _visibility: followersDescription: "Dành riêng cho người theo dõi" specified: "Nhắn riêng" specifiedDescription: "Chỉ người được nhắc đến mới thấy" - localOnly: "Chỉ trên máy chủ" - localOnlyDescription: "Không hiển thị với người ở máy chủ khác" + disableFederation: "Không liên hợp" + disableFederationDescription: "Không đưa tin cho chủ máy khác" _postForm: replyPlaceholder: "Trả lời tút này" quotePlaceholder: "Trích dẫn tút này" @@ -1353,7 +1523,7 @@ _postForm: b: "Hôm nay bạn có gì vui?" c: "Bạn đang nghĩ gì?" d: "Bạn muốn nói gì?" - e: "Bắt đầu viết..." + e: "Cứ viết trên đây" f: "Đang chờ bạn viết..." _profile: name: "Tên" @@ -1369,6 +1539,7 @@ _profile: changeBanner: "Đổi ảnh bìa" _exportOrImport: allNotes: "Toàn bộ tút" + favoritedNotes: "Bài viết đã thích" followingList: "Đang theo dõi" muteList: "Ẩn" blockingList: "Chặn" @@ -1407,7 +1578,16 @@ _timelines: social: "Xã hội" global: "Liên hợp" _play: + new: "Tạo Play mới" + edit: "Edit play" + created: "Bạn vừa tạo play rồi" + updated: "Bạn vừa cập nhật play rồi" + deleted: "Bạn vừa xóa play rồi" + pageSetting: "Cài đặt play" + editThisPage: "Edit play này" viewSource: "Xem mã nguồn" + my: "Play của mình" + liked: "Play đã thích" featured: "Nổi tiếng" title: "Tựa đề" script: "Kịch bản" @@ -1471,14 +1651,13 @@ _notification: youGotReply: "{name} trả lời bạn" youGotQuote: "{name} trích dẫn tút của bạn" youRenoted: "{name} đăng lại tút của bạn" - youGotMessagingMessageFromUser: "{name} nhắn tin cho bạn" - youGotMessagingMessageFromGroup: "Một tin nhắn trong nhóm {name}" youWereFollowed: "đã theo dõi bạn" youReceivedFollowRequest: "Bạn vừa có một yêu cầu theo dõi" yourFollowRequestAccepted: "Yêu cầu theo dõi của bạn đã được chấp nhận" - youWereInvitedToGroup: "Bạn đã được mời tham gia nhóm" pollEnded: "Cuộc bình chọn đã kết thúc" + unreadAntennaNote: "Ăng ten" emptyPushNotificationMessage: "Đã cập nhật thông báo đẩy" + achievementEarned: "Hoàn thành Achievement" _types: all: "Toàn bộ" follow: "Đang theo dõi" @@ -1490,7 +1669,7 @@ _notification: pollEnded: "Bình chọn kết thúc" receiveFollowRequest: "Yêu cầu theo dõi" followRequestAccepted: "Yêu cầu theo dõi được chấp nhận" - groupInvited: "Mời vào nhóm" + achievementEarned: "Hoàn thành Achievement" app: "Từ app liên kết" _actions: followBack: "đã theo dõi lại bạn" @@ -1523,3 +1702,6 @@ _deck: channel: "Kênh" mentions: "Lượt nhắc" direct: "Nhắn riêng" +_dialog: + charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}" + charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 9a63bdec4e..517f5a9efa 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -103,6 +103,8 @@ renoted: "已转发。" cantRenote: "该帖无法转发。" cantReRenote: "转发无法被再次转发。" quote: "引用" +inChannelRenote: "在频道内转发" +inChannelQuote: "在频道内引用" pinnedNote: "已置顶的帖子" pinned: "置顶" you: "您" @@ -129,6 +131,7 @@ unblockConfirm: "确定要解除拉黑吗?" suspendConfirm: "要冻结吗?" unsuspendConfirm: "要解除冻结吗?" selectList: "选择列表" +selectChannel: "选择频道" selectAntenna: "选择天线" selectWidget: "选择小工具" editWidgets: "编辑部件" @@ -256,6 +259,8 @@ noMoreHistory: "没有更多的历史记录" startMessaging: "添加聊天" nUsersRead: "{n}人已读" agreeTo: "勾选则表示已阅读并同意{0}" +agreeBelow: "同意以下观点" +basicNotesBeforeCreateAccount: "基本注意事项" tos: "服务条款" start: "开始" home: "首页" @@ -388,16 +393,19 @@ about: "关于" aboutMisskey: "关于 Misskey" administrator: "管理员" token: "Token (令牌)" -twoStepAuthentication: "两步验证" +2fa: "双因素认证" +totp: "身份验证应用" +totpDescription: "使用认证应用输入一次性密码。" moderator: "监察员" moderation: "管理" nUsersMentioned: "{n} 被提到" +securityKeyAndPasskey: "安全密钥/密码" securityKey: "安全密钥" -securityKeyName: "密钥名称" -registerSecurityKey: "注册硬件安全密钥" lastUsed: "最后使用:" +lastUsedAt: "最后使用: {t}" unregister: "删除账户" passwordLessLogin: "无密码登录" +passwordLessLoginDescription: "不使用密码,仅使用安全密钥或Passkey登录" resetPassword: "重置密码" newPasswordIs: "新的密码是「{password}」" reduceUiAnimation: "减少UI动画" @@ -412,24 +420,15 @@ markAsReadAllTalkMessages: "将所有聊天标记为已读" help: "帮助" inputMessageHere: "在此键入信息" close: "关闭" -group: "群组" -groups: "群组" -createGroup: "创建群组" -ownedGroups: "拥有的群组" -joinedGroups: "已加入的群组" invites: "邀请" -groupName: "群组名" members: "成员" transfer: "转让" -messagingWithUser: "与用户聊天" -messagingWithGroup: "与群组聊天" title: "标题" text: "文本" enable: "启用" next: "下一个" retype: "重新输入" noteOf: "{user}的帖子" -inviteToGroup: "群组邀请" quoteAttached: "已引用" quoteQuestion: "是否引用此链接内容?" noMessagesYet: "现在没有新的聊天" @@ -451,20 +450,18 @@ passwordMatched: "密码一致" passwordNotMatched: "密码不一致" signinWith: "以{x}登录" signinFailed: "无法登录,请检查您的用户名和密码是否正确。" -tapSecurityKey: "轻触硬件安全密钥" or: "或者" language: "语言" uiLanguage: "显示语言" -groupInvited: "您有新的群组邀请" aboutX: "关于 {x}" emojiStyle: "emoji 的样式" native: "原生" disableDrawer: "不显示抽屉菜单" -youHaveNoGroups: "没有群组" -joinOrCreateGroup: "请加入一个现有的群组,或者创建新群组。" +showNoteActionsOnlyHover: "仅在悬停时显示帖子操作" noHistory: "没有历史记录" signinHistory: "登录历史" -disableAnimatedMfm: "禁用MFM动画" +enableAdvancedMfm: "启用扩展MFM" +enableAnimatedMfm: "启用MFM动画" doing: "正在进行" category: "类别" tags: "标签" @@ -679,7 +676,7 @@ driveFilesCount: "网盘的文件数" driveUsage: "网盘的空间用量" noCrawle: "要求搜索引擎不索引该用户" noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。" -lockedAccountInfo: "即使通过了关注请求,只要您不将帖子可见范围设置成“关注者”,任何人都可以看到您的帖子。" +lockedAccountInfo: "即使启用该功能,只要您不将帖子可见范围设置为“仅关注者”,任何人都还是可以看到您的帖子。" alwaysMarkSensitive: "默认将媒体文件标记为敏感内容" loadRawImages: "添加附件图像的缩略图时使用原始图像质量" disableShowingAnimatedImages: "不播放动画" @@ -783,6 +780,7 @@ popularPosts: "热门投稿" shareWithNote: "在帖子中分享" ads: "广告" expiration: "截止时间" +startingperiod: "开始时间" memo: "便笺" priority: "优先级" high: "高" @@ -809,12 +807,13 @@ translatedFrom: "从 {x} 翻译" accountDeletionInProgress: "正在删除账户" usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" aiChanMode: "小蓝模式" -keepCw: "保留CW" +keepCw: "回复时维持隐藏内容" pubSub: "Pub/Sub账户" lastCommunication: "最近通信" resolved: "已解决" unresolved: "未解决" breakFollow: "移除关注者" +breakFollowConfirm: "你想取消关注吗?" itsOn: "已开启" itsOff: "已关闭" emailRequiredForSignup: "注册账户需要电子邮件地址" @@ -834,8 +833,6 @@ deleteAccountConfirm: "将要删除账户。是否确认?" incorrectPassword: "密码错误" voteConfirm: "确定投给“{choice}” ?" hide: "隐藏" -leaveGroup: "离开群组" -leaveGroupConfirm: "确定离开「{name}」?" useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示" welcomeBackWithName: "欢迎回来,{name}" clickToFinishEmailVerification: "点击 [{ok}] 完成电子邮件地址认证。" @@ -851,16 +848,20 @@ instanceDefaultLightTheme: "实例默认浅色主题" instanceDefaultDarkTheme: "实例默认深色主题" instanceDefaultThemeDescription: "以对象格式键入主题代码" mutePeriod: "屏蔽期限" +period: "截止时间" indefinitely: "永久" tenMinutes: "10分钟" oneHour: "1小时" oneDay: "1天" oneWeek: "1周" +oneMonth: "1 个月" reflectMayTakeTime: "可能需要一些时间才能体现出效果。" failedToFetchAccountInformation: "获取账户信息失败" rateLimitExceeded: "已超過速率限制" cropImage: "剪裁图像" cropImageAsk: "是否要裁剪图像?" +cropYes: "去裁剪" +cropNo: "就这样吧!" file: "文件" recentNHours: "最近{n}小时" recentNDays: "最近{n}天" @@ -939,6 +940,21 @@ cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用, preset: "預設值" selectFromPresets: "從預設值中選擇" achievements: "成就" +gotInvalidResponseError: "服务器无应答" +gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。" +thisPostMayBeAnnoying: "这个帖子可能会让其他人感到困扰。" +thisPostMayBeAnnoyingHome: "发到首页" +thisPostMayBeAnnoyingCancel: "取消" +thisPostMayBeAnnoyingIgnore: "就这样发布" +collapseRenotes: "省略显示已经看过的转发内容" +internalServerError: "内部服务器错误" +internalServerErrorDescription: "内部服务器发生了预期外的错误" +copyErrorInfo: "复制错误信息" +joinThisServer: "在本实例上注册" +exploreOtherServers: "探索其他实例" +letsLookAtTimeline: "时间线" +disableFederationWarn: "联合被禁用。 禁用它并不能使帖子变成私人的。 在大多数情况下,这个选项不需要被启用。" +invitationRequiredToRegister: "此实例目前只允许拥有邀请码的人注册。" _achievements: earnedAt: "达成时间" _types: @@ -1111,7 +1127,7 @@ _achievements: title: "休息一下!" description: "启动客户端超过30分钟" _noteDeletedWithin1min: - title: "无话可说" + title: "欲言又止" description: "发帖后一分钟内就将其删除" _postedAtLateNight: title: "夜猫子" @@ -1122,7 +1138,7 @@ _achievements: description: "在0点发布一篇帖子" flavor: "嘣 嘣 嘣 Biu——!" _selfQuote: - title: "自我提及" + title: "自我引用" description: "引用了自己的帖子" _htl20npm: title: "流动的时间线" @@ -1323,72 +1339,6 @@ _nsfw: respect: "隐藏敏感内容" ignore: "不隐藏敏感内容" force: "总是隐藏内容" -_mfm: - cheatSheet: "MFM代码速查表" - intro: "MFM是一种在Misskey中的各个位置使用的专用标记语言。在这里您可以看到MFM中可用的语法列表。" - dummy: "通过Misskey扩展联邦宇宙的世界" - mention: "提及" - mentionDescription: "可以使用 @+用户名 来指示特定用户" - hashtag: "话题标签" - hashtagDescription: "可以使用井号+文字来表示话题标签。" - url: "URL" - urlDescription: "可以表示URL地址。" - link: "链接" - linkDescription: "可以将部分文字和URL关联起来。" - bold: "粗体" - boldDescription: "可以将文字显示为粗体来表示强调。" - small: "缩小" - smallDescription: "可以使内容文字变小、变淡。" - center: "居中" - centerDescription: "可以将内容居中显示。" - inlineCode: "代码(内嵌)" - inlineCodeDescription: "将文字中的程序代码语法高亮显示。" - blockCode: "代码(块)" - blockCodeDescription: "语法高亮显示整块程序代码。" - inlineMath: "数学公式(内嵌)" - inlineMathDescription: "显示内嵌的KaTex公式。" - blockMath: "数学公式(块)" - blockMathDescription: "显示整块的多行KaTex数学公式。" - quote: "引用" - quoteDescription: "可以用来表示引用的内容。" - emoji: "自定义表情符号" - emojiDescription: "可以将自定义表情符号使用冒号括起来,就可以显示自定义表情符号了。" - search: "搜索" - searchDescription: "显示含有搜索内容示例的搜索框。" - flip: "翻转" - flipDescription: "将内容上下或左右翻转。" - jelly: "动画(果冻)" - jellyDescription: "显示果冻一样的动画效果。" - tada: "动画(锵锵)" - tadaDescription: "显示\"锵锵!\"的动画效果。" - jump: "动画(跳动)" - jumpDescription: "显示跳动的动画效果。" - bounce: "动画(弹性)" - bounceDescription: "显示弹性一样的动画效果。" - shake: "动画(摇晃)" - shakeDescription: "显示摇晃的动画效果。" - twitch: "动画(颤抖)" - twitchDescription: "显示强烈颤抖的动画效果。" - spin: "动画(旋转)" - spinDescription: "显示旋转的动画效果。" - x2: "大" - x2Description: "以大尺寸显示内容。" - x3: "非常大" - x3Description: "以更大尺寸显示内容。" - x4: "最大" - x4Description: "以最大尺寸显示内容。" - blur: "模糊" - blurDescription: "产生模糊效果。将鼠标指针放在上面即可将内容显示出来。" - font: "字体" - fontDescription: "可以设置内容所使用的字体。" - rainbow: "彩虹" - rainbowDescription: "用彩虹色来显示内容。" - sparkle: "闪光" - sparkleDescription: "添加发光粒子效果。" - rotate: "旋转" - rotateDescription: "旋转指定的角度。" - plain: "简洁" - plainDescription: "禁用所有内部语法。" _instanceTicker: none: "不显示" remote: "仅远程用户" @@ -1484,9 +1434,9 @@ _theme: infoFg: "信息文本" infoWarnBg: "警告背景" infoWarnFg: "警告文本" - cwBg: "CW 按钮背景" - cwFg: "CW 按钮文本" - cwHoverBg: "CW 按钮背景(悬停)" + cwBg: "隐藏内容按钮背景" + cwFg: "隐藏内容按钮文本" + cwHoverBg: "隐藏内容按钮背景(悬停)" toastBg: "Toast通知背景" toastFg: "Toast通知文本" buttonBg: "按钮背景" @@ -1518,6 +1468,7 @@ _ago: weeksAgo: "{n}周前" monthsAgo: "{n}月前" yearsAgo: "{n}年前" + invalid: "没有" _time: second: "秒" minute: "分" @@ -1551,14 +1502,29 @@ _tutorial: step8_3: "您也可以稍后再更改通知设置。" _2fa: alreadyRegistered: "此设备已被注册" - registerDevice: "注册设备" - registerKey: "注册密钥" + registerTOTP: "开始设置认证应用" + passwordToTOTP: "请输入您的密码" step1: "首先,在您的设备上安装验证应用,例如{a}或{b}。" step2: "然后,扫描屏幕上显示的二维码。" + step2Click: "通过点击QR码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册" step2Url: "在桌面应用程序中输入以下URL:" + step3Title: "输入验证码" step3: "输入您的应用提供的动态口令以完成设置。" step4: "从现在开始,任何登录操作都将要求您提供动态口令。" + securityKeyNotSupported: "您的浏览器不支持安全密钥。" + registerTOTPBeforeKey: "要注册安全密钥或Passkey,请先设置验证器应用程序。" securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。" + chromePasskeyNotSupported: "目前不支持 Chrome 的Passkey。" + registerSecurityKey: "注册安全密钥或Passkey" + securityKeyName: "输入密钥名称" + tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或Passkey。" + removeKey: "删除安全密钥" + removeKeyConfirm: "您确定要删除 {name} 吗?" + whyTOTPOnlyRenew: "如果注册了安全密钥,则无法取消验证器应用程序上的设置。" + renewTOTP: "重置验证器应用程序" + renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效" + renewTOTPOk: "重新配置" + renewTOTPCancel: "不用,谢谢" _permissions: "read:account": "查看账户信息" "write:account": "更改帐户信息" @@ -1593,18 +1559,20 @@ _permissions: "read:gallery-likes": "读取喜欢的图片" "write:gallery-likes": "操作喜欢的图片" _auth: + shareAccessTitle: "应用程序授权许可" shareAccess: "您要授权允许“{name}”访问您的帐户吗?" shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?" + permission: "{name}需要以下权限" permissionAsk: "这个应用程序需要以下权限" pleaseGoBack: "请返回到应用程序" callback: "回到应用程序" denied: "拒绝访问" + pleaseLogin: "在对应用进行授权许可之前,请先登录" _antennaSources: all: "所有帖子" homeTimeline: "已关注用户的帖子" users: "来自指定用户的帖子" userList: "来自指定列表中的帖子" - userGroup: "来自指定群组中用户的帖子" _weekday: sunday: "星期日" monday: "星期一" @@ -1679,8 +1647,8 @@ _visibility: followersDescription: "仅发送至关注者" specified: "指定用户" specifiedDescription: "仅发送至指定用户" - localOnly: "仅限本地" - localOnlyDescription: "对远程用户不可见" + disableFederation: "不参与联合" + disableFederationDescription: "不发送到其他实例" _postForm: replyPlaceholder: "回复这个帖子..." quotePlaceholder: "引用这个帖子..." @@ -1696,7 +1664,7 @@ _profile: name: "昵称" username: "用户名" description: "个人简介" - youCanIncludeHashtags: "你可以在个人简介中包含一个#标签。" + youCanIncludeHashtags: "你可以在个人简介中包含一些#标签。" metadata: "附加信息" metadataEdit: "附加信息编辑" metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。" @@ -1818,12 +1786,9 @@ _notification: youGotReply: "来自{name}的回复" youGotQuote: "来自{name}的引用" youRenoted: "来自{name}的转发" - youGotMessagingMessageFromUser: "来自{name}的聊天" - youGotMessagingMessageFromGroup: "来自{name}的群聊" youWereFollowed: "关注了你。" youReceivedFollowRequest: "您有新的关注请求" yourFollowRequestAccepted: "您的关注请求已通过" - youWereInvitedToGroup: "您有新的群组邀请" pollEnded: "问卷调查结果已生成。" unreadAntennaNote: "天线 {name}" emptyPushNotificationMessage: "推送通知已更新" @@ -1839,7 +1804,7 @@ _notification: pollEnded: "问卷调查结束" receiveFollowRequest: "收到关注请求" followRequestAccepted: "关注请求已通过" - groupInvited: "加入群组邀请" + achievementEarned: "取得的成就" app: "关联应用的通知" _actions: followBack: "回关" @@ -1872,3 +1837,6 @@ _deck: channel: "频道" mentions: "提及" direct: "指定用户" +_dialog: + charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}" + charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 570afce8fa..43ab334be7 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -46,7 +46,7 @@ copyContent: "複製內容" copyLink: "複製連結" delete: "刪除" deleteAndEdit: "刪除並編輯" -deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有情感、轉發和回覆也將會消失。" +deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。" addToList: "加入至清單" sendMessage: "發送訊息" copyRSS: "複製RSS" @@ -103,14 +103,16 @@ renoted: "轉傳成功" cantRenote: "無法轉發此貼文。" cantReRenote: "無法轉傳之前已經轉傳過的內容。" quote: "引用" +inChannelRenote: "在頻道內轉發" +inChannelQuote: "在頻道內引用" pinnedNote: "已置頂的貼文" pinned: "置頂" you: "您" clickToShow: "按一下以顯示" sensitive: "敏感內容" add: "新增" -reaction: "情感" -reactions: "情感" +reaction: "反應" +reactions: "反應" reactionSetting: "在選擇器中顯示反應" reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。" rememberNoteVisibility: "記住貼文可見性" @@ -129,6 +131,7 @@ unblockConfirm: "確定解除封鎖此用戶?" suspendConfirm: "確定凍結此帳號?" unsuspendConfirm: "確定解凍此帳號?" selectList: "選擇清單" +selectChannel: "選擇頻道" selectAntenna: "選擇天線" selectWidget: "選擇小工具" editWidgets: "編輯小工具" @@ -210,7 +213,7 @@ default: "預設" defaultValueIs: "預設值:{value}" noCustomEmojis: "沒有自訂的表情符號" noJobs: "沒有任務" -federating: "整合搜索中" +federating: "聯邦運作中" blocked: "已封鎖" suspended: "已凍結" all: "全部" @@ -256,6 +259,8 @@ noMoreHistory: "沒有更多歷史紀錄" startMessaging: "開始聊天" nUsersRead: "{n}人已讀" agreeTo: "我同意{0}" +agreeBelow: "同意以下內容" +basicNotesBeforeCreateAccount: "基本注意事項" tos: "使用條款" start: "開始" home: "首頁" @@ -388,16 +393,19 @@ about: "關於" aboutMisskey: "關於 Misskey" administrator: "管理員" token: "權杖" -twoStepAuthentication: "兩階段驗證" +2fa: "雙因素驗證" +totp: "驗證應用程式" +totpDescription: "以驗證應用程式輸入一次性密碼" moderator: "審查員" moderation: "審查" nUsersMentioned: "提到了{n}" +securityKeyAndPasskey: "安全金鑰・Passkey" securityKey: "安全金鑰" -securityKeyName: "金鑰名稱" -registerSecurityKey: "註冊安全金鑰" lastUsed: "上次使用" +lastUsedAt: "最後使用:{t}" unregister: "註銷帳號" passwordLessLogin: "設置無密碼登入" +passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入" resetPassword: "重置密碼" newPasswordIs: "新密碼為「{password}」" reduceUiAnimation: "減少介面的動態視覺" @@ -412,24 +420,15 @@ markAsReadAllTalkMessages: "標記所有訊息為已讀" help: "幫助" inputMessageHere: "在此輸入訊息" close: "關閉" -group: "群組" -groups: "群組" -createGroup: "創建群組" -ownedGroups: "擁有的群組" -joinedGroups: "群組成員" invites: "邀請" -groupName: "群組名稱" members: "成員" transfer: "轉讓" -messagingWithUser: "與其他使用者聊天" -messagingWithGroup: "發送訊息至群組" title: "標題" text: "文字" enable: "啟用" next: "下一步" -retype: "重新輸入" +retype: "再次輸入" noteOf: "{user}的貼文" -inviteToGroup: "邀請至群組" quoteAttached: "引用" quoteQuestion: "是否要引用?" noMessagesYet: "沒有訊息" @@ -451,20 +450,18 @@ passwordMatched: "密碼一致" passwordNotMatched: "密碼不一致" signinWith: "以{x}登錄" signinFailed: "登入失敗。 請檢查使用者名稱和密碼。" -tapSecurityKey: "點擊安全密鑰" or: "或者" language: "語言" uiLanguage: "介面語言" -groupInvited: "您有新的群組邀請" aboutX: "關於{x}" emojiStyle: "表情符號的風格" native: "原生" disableDrawer: "不顯示下拉式選單" -youHaveNoGroups: "找不到群組" -joinOrCreateGroup: "請加入現有群組,或創建新群組。" +showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項" noHistory: "沒有歷史紀錄" signinHistory: "登入歷史" -disableAnimatedMfm: "禁用MFM動畫" +enableAdvancedMfm: "啟用高級MFM" +enableAnimatedMfm: "啟用MFM動畫" doing: "正在進行" category: "類別" tags: "標籤" @@ -669,8 +666,8 @@ repliedCount: "回覆數量" renotedCount: "轉發次數" followingCount: "正在跟隨的用戶數量" followersCount: "跟隨者數量" -sentReactionsCount: "情感發送次數" -receivedReactionsCount: "情感收到次數" +sentReactionsCount: "反應發送次數" +receivedReactionsCount: "收到反應次數" pollVotesCount: "已統計的投票數" pollVotedCount: "已投票數" yes: "確定" @@ -783,6 +780,7 @@ popularPosts: "熱門的貼文" shareWithNote: "在貼文中分享" ads: "廣告" expiration: "期限" +startingperiod: "開始期間" memo: "備忘錄" priority: "優先級" high: "高" @@ -815,6 +813,7 @@ lastCommunication: "最近的通信" resolved: "已解決" unresolved: "未解決" breakFollow: "移除追蹤者" +breakFollowConfirm: "確定要取消被追隨嗎?" itsOn: "已開啟" itsOff: "已關閉" emailRequiredForSignup: "註冊帳戶需要電子郵件地址" @@ -834,8 +833,6 @@ deleteAccountConfirm: "將要刪除帳戶。是否確定?" incorrectPassword: "密碼錯誤。" voteConfirm: "確定投給「{choice}」?" hide: "隱藏" -leaveGroup: "離開群組" -leaveGroupConfirm: "確定離開「{name}」?" useDrawerReactionPickerForMobile: "在移動設備上使用抽屜顯示" welcomeBackWithName: "歡迎回來,{name}" clickToFinishEmailVerification: "點擊 [{ok}] 完成電子郵件地址認證。" @@ -851,16 +848,20 @@ instanceDefaultLightTheme: "實例預設的淺色主題" instanceDefaultDarkTheme: "實例預設的深色主題" instanceDefaultThemeDescription: "輸入物件形式的主题代碼" mutePeriod: "靜音的期限" +period: "期限" indefinitely: "無期限" tenMinutes: "10分鐘" oneHour: "1小時" oneDay: "1天" oneWeek: "1週" +oneMonth: "1個月" reflectMayTakeTime: "可能需要一些時間才會出現效果。" failedToFetchAccountInformation: "取得帳戶資訊失敗" rateLimitExceeded: "已超過速率限制" cropImage: "圖片裁剪" cropImageAsk: "要剪裁圖片嗎?" +cropYes: "裁剪" +cropNo: "使用原圖" file: "檔案" recentNHours: "過去{n}小時" recentNDays: "過去{n}天" @@ -941,6 +942,19 @@ selectFromPresets: "從預設值中選擇" achievements: "成就" gotInvalidResponseError: "伺服器的回應無效" gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。" +thisPostMayBeAnnoying: "這篇貼文可能會造成別人的困擾。" +thisPostMayBeAnnoyingHome: "發布到首頁" +thisPostMayBeAnnoyingCancel: "退出" +thisPostMayBeAnnoyingIgnore: "直接發布貼文" +collapseRenotes: "省略顯示已看過的轉發貼文" +internalServerError: "內部伺服器錯誤" +internalServerErrorDescription: "內部伺服器發生了非預期的錯誤。" +copyErrorInfo: "複製錯誤資訊" +joinThisServer: "在此伺服器上註冊" +exploreOtherServers: "探索其他伺服器" +letsLookAtTimeline: "看看時間軸" +disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼文不公開,在大多數情況下,不需要啟用這個選項。" +invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。" _achievements: earnedAt: "獲得日期" _types: @@ -1085,7 +1099,7 @@ _achievements: title: "成群結隊" description: "跟隨者超過50人了" _followers100: - title: "紅人" + title: "熱門人物" description: "跟隨者超過100人了" _followers300: title: "請排成一排" @@ -1143,7 +1157,7 @@ _achievements: description: "試圖遞迴套入雲端硬碟資料夾" _reactWithoutRead: title: "有好好讀過嗎?" - description: "對包含100字以上內容的貼文做出情感反應" + description: "對包含100字以上內容的貼文在3秒以內做出反應" _clickedClickHere: title: "點擊這裡" description: "已點擊這裡了" @@ -1325,72 +1339,6 @@ _nsfw: respect: "隱藏敏感內容" ignore: "不隱藏敏感內容" force: "隱藏所有內容" -_mfm: - cheatSheet: "MFM代碼小抄" - intro: "MFM是Misskey專用的標記語言,可以在Misskey中的各個位置使用。 您可以這裏看到MFM可用語法列表。" - dummy: "Misskey拓展了Fediverse的世界" - mention: "提及" - mentionDescription: "透過 @+用戶名 來標示特定使用者。" - hashtag: "#tag" - hashtagDescription: "可以使用\"#\"符號後加文字表示話題標籤。" - url: "URL" - urlDescription: "可以展示URL位址。" - link: "鏈接" - linkDescription: "您可以將特定範圍的文章與 URL 相關聯。 " - bold: "粗體" - boldDescription: "可以將文字顯示为粗體来強調。" - small: "縮小" - smallDescription: "可以使內容文字變小、變淡。" - center: "置中" - centerDescription: "可以將內容置中顯示。" - inlineCode: "程式碼(内嵌)" - inlineCodeDescription: "在行內用高亮度顯示,例如程式碼語法。" - blockCode: "程式碼(區塊)" - blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。" - inlineMath: "數學公式(內嵌)" - inlineMathDescription: "顯示內嵌的KaTex數學公式。" - blockMath: "數學公式(方塊)" - blockMathDescription: "以區塊顯示複數行的KaTex數學式。" - quote: "引用" - quoteDescription: "可以用來表示引用的内容。" - emoji: "自訂表情符號" - emojiDescription: "您可以通過將自定義表情符號名稱括在冒號中來顯示自定義表情符號。 " - search: "搜尋" - searchDescription: "您可以顯示所輸入的搜索框。" - flip: "翻轉" - flipDescription: "將內容上下或左右翻轉。" - jelly: "動畫(果凍)" - jellyDescription: "顯示果凍一樣的動畫效果。" - tada: "動畫(鏘~)" - tadaDescription: "顯示「鏘~!」這種感覺的動畫效果。" - jump: "動畫(跳動)" - jumpDescription: "顯示跳動的動畫效果。" - bounce: "動畫(反彈)" - bounceDescription: "顯示有彈性的動畫效果。" - shake: "動畫(搖晃)" - shakeDescription: "顯示顫抖的動畫效果。" - twitch: "動畫(顫抖)" - twitchDescription: "顯示強烈顫抖的動畫效果。" - spin: "動畫(旋轉)" - spinDescription: "顯示旋轉的動畫效果。" - x2: "大" - x2Description: "放大顯示內容。" - x3: "較大" - x3Description: "放大顯示內容。" - x4: "最大" - x4Description: "將顯示內容放至最大。" - blur: "模糊" - blurDescription: "產生模糊效果。将游標放在上面即可將内容顯示出來。" - font: "字型" - fontDescription: "您可以設定顯示內容的字型" - rainbow: "彩虹" - rainbowDescription: "用彩虹色來顯示內容。" - sparkle: "閃閃發光" - sparkleDescription: "添加閃閃發光的粒子效果。" - rotate: "旋轉" - rotateDescription: "以指定的角度旋轉。" - plain: "簡潔" - plainDescription: "停用全部的內部語法。" _instanceTicker: none: "隱藏" remote: "向遠端使用者顯示" @@ -1520,6 +1468,7 @@ _ago: weeksAgo: "{n}周前" monthsAgo: "{n}個月前" yearsAgo: "{n}年前" + invalid: "未發現" _time: second: "秒" minute: "分鐘" @@ -1543,7 +1492,7 @@ _tutorial: step5_3: "想要追隨其他人,只要點擊他們的大頭貼並按「追隨」即可。" step5_4: "如果使用者的名字旁有鎖頭的圖示,代表他們需要手動核准你的追隨請求。" step6_1: "現在你可以在時間軸上看到其他用戶的貼文。" - step6_2: "你也可以對別人的貼文作出「情感」,作出簡單的回覆。" + step6_2: "你也可以對別人的貼文作出「反應」,作出簡單的回覆。" step6_3: "在他人的貼文按下\"+\"圖標,即可選擇喜好的表情符號進行回應。" step7_1: "以上為Misskey的基本操作說明,教學在此告一段落。辛苦了。" step7_2: "歡迎到{help}來瞭解更多Misskey相關介紹。" @@ -1553,14 +1502,29 @@ _tutorial: step8_3: "通知的設定可以在之後變更。" _2fa: alreadyRegistered: "此設備已經被註冊過了" - registerDevice: "註冊裝置" - registerKey: "註冊鍵" + registerTOTP: "開始設定驗證應用程式" + passwordToTOTP: "請輸入密碼" step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。" step2: "然後,掃描螢幕上的QR code。" + step2Click: "點擊QR code,可以使用設備上安裝的驗證應用程式或金鑰環進行註冊。" step2Url: "在桌面版應用中,請輸入以下的URL:" + step3Title: "輸入驗證碼" step3: "輸入您的App提供的權杖以完成設定。" step4: "從現在開始,任何登入操作都將要求您提供權杖。" + securityKeyNotSupported: "您的瀏覽器不支援安全金鑰。" + registerTOTPBeforeKey: "要註冊安全金鑰・Passkey,請先設定驗證應用程式。" securityKeyInfo: "您可以設定使用支援FIDO2的硬體安全鎖、終端設備的指纹認證或者PIN碼來登入。" + chromePasskeyNotSupported: "目前不支援Chrome的Passkey。" + registerSecurityKey: "註冊安全金鑰・Passkey" + securityKeyName: "輸入金鑰名稱" + tapSecurityKey: "按照瀏覽器的說明操作,註冊安全金鑰和Passkey。" + removeKey: "刪除安全金鑰" + removeKeyConfirm: "要刪除{name}嗎?" + whyTOTPOnlyRenew: "如果註冊了安全金鑰,則無法解除驗證應用程式的設定。" + renewTOTP: "重設驗證應用程式" + renewTOTPConfirm: "目前驗證應用程式的驗證碼將無法使用。" + renewTOTPOk: "重設" + renewTOTPCancel: "現在不要" _permissions: "read:account": "查看我的帳戶資訊" "write:account": "更改我的帳戶資訊" @@ -1579,8 +1543,8 @@ _permissions: "write:notes": "撰寫或刪除貼文" "read:notifications": "查看通知" "write:notifications": "編輯通知" - "read:reactions": "查看情感" - "write:reactions": "編輯情感" + "read:reactions": "查看反應" + "write:reactions": "編輯反應" "write:votes": "投票" "read:pages": "顯示頁面" "write:pages": "編輯頁面" @@ -1595,18 +1559,20 @@ _permissions: "read:gallery-likes": "讀取喜歡的圖片" "write:gallery-likes": "操作喜歡的圖片" _auth: + shareAccessTitle: "應用程式的存取權限" shareAccess: "要授權「“{name}”」存取您的帳戶嗎?" shareAccessAsk: "您確定要授權這個應用程式使用您的帳戶嗎?" + permission: "{name}要求以下的權限" permissionAsk: "此應用程式需要以下權限" pleaseGoBack: "請返回至應用程式" callback: "回到應用程式" denied: "拒絕訪問" + pleaseLogin: "必須登入以提供應用程式的存取權限。" _antennaSources: all: "全部貼文" homeTimeline: "來自已追隨使用者的貼文" users: "來自特定使用者的貼文" userList: "來自特定清單中的貼文" - userGroup: "來自特定群組的貼文" _weekday: sunday: "週日" monday: "週一" @@ -1630,7 +1596,7 @@ _widgets: photos: "照片" digitalClock: "電子時鐘" unixClock: "UNIX時間" - federation: "聯邦宇宙" + federation: "站台聯邦" instanceCloud: "實例雲" postForm: "發佈窗口" slideshow: "幻燈片" @@ -1681,8 +1647,8 @@ _visibility: followersDescription: "僅發送至關注者" specified: "指定使用者" specifiedDescription: "僅發送至指定使用者" - localOnly: "僅限本地" - localOnlyDescription: "對遠端使用者隱藏" + disableFederation: "停用聯邦" + disableFederationDescription: "不要傳遞給其他實例" _postForm: replyPlaceholder: "回覆此貼文..." quotePlaceholder: "引用此貼文..." @@ -1820,12 +1786,9 @@ _notification: youGotReply: "{name}回覆了您" youGotQuote: "{name}引用了您" youRenoted: "{name} 轉發了你的貼文" - youGotMessagingMessageFromUser: "{name}發送給您的訊息" - youGotMessagingMessageFromGroup: "{name}發送給您的訊息" youWereFollowed: "您有新的追隨者" youReceivedFollowRequest: "您有新的追隨請求" yourFollowRequestAccepted: "您的追隨請求已通過" - youWereInvitedToGroup: "您有新的群組邀請" pollEnded: "問卷調查已產生結果" unreadAntennaNote: "天線 {name}" emptyPushNotificationMessage: "推送通知已更新" @@ -1841,7 +1804,7 @@ _notification: pollEnded: "問卷調查結束" receiveFollowRequest: "已收到追隨請求" followRequestAccepted: "追隨請求已接受" - groupInvited: "加入社群邀請" + achievementEarned: "獲得成就" app: "應用程式通知" _actions: followBack: "回關" @@ -1874,3 +1837,6 @@ _deck: channel: "頻道" mentions: "提及" direct: "指定使用者" +_dialog: + charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}" + charactersBelow: "低於最少字數!現在 {current} / 限制 {max}" diff --git a/package.json b/package.json index 578bcb76a4..a2d04030f2 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "13.5.5", + "version": "13.9.1", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@7.24.3", + "packageManager": "pnpm@7.27.0", "workspaces": [ "packages/frontend", "packages/backend", @@ -16,10 +16,11 @@ "scripts": { "build-pre": "node ./scripts/build-pre.js", "build": "pnpm build-pre && pnpm -r build && pnpm gulp", - "start": "cd packages/backend && node ./built/boot/index.js", + "start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js", "init": "pnpm migrate", "migrate": "cd packages/backend && pnpm migrate", + "check:connect": "cd packages/backend && pnpm check:connect", "migrateandstart": "pnpm migrate && pnpm start", "gulp": "pnpm exec gulp build", "watch": "pnpm dev", @@ -54,12 +55,12 @@ "devDependencies": { "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", - "@typescript-eslint/eslint-plugin": "5.51.0", - "@typescript-eslint/parser": "5.51.0", + "@typescript-eslint/eslint-plugin": "5.53.0", + "@typescript-eslint/parser": "5.53.0", "cross-env": "7.0.3", - "cypress": "12.5.1", - "eslint": "8.33.0", - "start-server-and-test": "1.15.3" + "cypress": "12.7.0", + "eslint": "8.35.0", + "start-server-and-test": "1.15.4" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.2.0" diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs index 5a06889dcd..f9fe4814e6 100644 --- a/packages/backend/.eslintrc.cjs +++ b/packages/backend/.eslintrc.cjs @@ -1,7 +1,7 @@ module.exports = { parserOptions: { tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], + project: ['./tsconfig.json', './test/tsconfig.json'], }, extends: [ '../shared/.eslintrc.js', diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc index 55a88456ef..08d4222d01 100644 --- a/packages/backend/.swcrc +++ b/packages/backend/.swcrc @@ -1,25 +1,23 @@ { - "$schema": "https://json.schemastore.org/swcrc", - "jsc": { - "parser": { - "syntax": "typescript", - "dynamicImport": true, - "decorators": true - }, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true - }, + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "dynamicImport": true, + "decorators": true + }, + "transform": { + "legacyDecorator": true, + "decoratorMetadata": true + }, "experimental": { "keepImportAssertions": true }, - "baseUrl": ".", + "baseUrl": "src", "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["*"] }, "target": "es2021" - }, - "minify": false + }, + "minify": false } diff --git a/packages/backend/assets/apple-touch-icon.png b/packages/backend/assets/apple-touch-icon.png index 947c513bbb..06ad3f1bb4 100644 Binary files a/packages/backend/assets/apple-touch-icon.png and b/packages/backend/assets/apple-touch-icon.png differ diff --git a/packages/backend/assets/icons/192.png b/packages/backend/assets/icons/192.png index 606b46d87c..15fd1e3731 100644 Binary files a/packages/backend/assets/icons/192.png and b/packages/backend/assets/icons/192.png differ diff --git a/packages/backend/assets/icons/512.png b/packages/backend/assets/icons/512.png index ba51546427..f2169ec9b0 100644 Binary files a/packages/backend/assets/icons/512.png and b/packages/backend/assets/icons/512.png differ diff --git a/packages/backend/check_connect.js b/packages/backend/check_connect.js new file mode 100644 index 0000000000..ed429c0254 --- /dev/null +++ b/packages/backend/check_connect.js @@ -0,0 +1,10 @@ +import { loadConfig } from './built/config.js'; +import { createRedisConnection } from './built/redis.js'; + +const config = loadConfig(); +const redis = createRedisConnection(config); + +redis.on('connect', () => redis.disconnect()); +redis.on('error', (e) => { + throw e; +}); diff --git a/packages/backend/jest-resolver.cjs b/packages/backend/jest-resolver.cjs deleted file mode 100644 index 4424b800dc..0000000000 --- a/packages/backend/jest-resolver.cjs +++ /dev/null @@ -1,14 +0,0 @@ -// https://github.com/facebook/jest/issues/12270#issuecomment-1194746382 - -const nativeModule = require('node:module'); - -function resolver(module, options) { - const { basedir, defaultResolver } = options; - try { - return defaultResolver(module, options); - } catch (error) { - return nativeModule.createRequire(basedir).resolve(module); - } -} - -module.exports = resolver; diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index f0a3dc16c2..6b1afec734 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -20,7 +20,7 @@ module.exports = { // collectCoverage: false, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: ['src/**/*.ts'], + collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'], // The directory where Jest should output its coverage files coverageDirectory: "coverage", @@ -83,8 +83,15 @@ module.exports = { // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module moduleNameMapper: { - "^@/(.*?).js": "/src/$1.ts", - '^(\\.{1,2}/.*)\\.js$': '$1', + // Do not resolve .wasm.js to .wasm by the rule below + '^(.+)\\.wasm\\.js$': '$1.wasm.js', + // SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule + // converts it again to `../../src/foo/bar` which then can be resolved to + // `.ts` files. + // See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225 + // TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can + // directly import `.ts` files without this hack. + '^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1', }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader @@ -112,7 +119,7 @@ module.exports = { // resetModules: false, // A path to a custom resolver - resolver: './jest-resolver.cjs', + // resolver: './jest-resolver.cjs', // Automatically restore mock state between every test restoreMocks: true, @@ -152,7 +159,8 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ "/test/unit/**/*.ts", - //"/test/e2e/**/*.ts" + "/src/**/*.test.ts", + "/test/e2e/**/*.ts", ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped @@ -199,4 +207,13 @@ module.exports = { // watchman: true, extensionsToTreatAsEsm: ['.ts'], + + testTimeout: 60000, + + // Let Jest kill the test worker whenever it grows too much + // (It seems there's a known memory leak issue in Node.js' vm.Script used by Jest) + // https://github.com/facebook/jest/issues/11956 + maxWorkers: 1, // Make it use worker (that can be killed and restarted) + logHeapUsage: true, // To debug when out-of-memory happens on CI + workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) }; diff --git a/packages/backend/migration/1676434944993-drop-group.js b/packages/backend/migration/1676434944993-drop-group.js new file mode 100644 index 0000000000..c856046eb9 --- /dev/null +++ b/packages/backend/migration/1676434944993-drop-group.js @@ -0,0 +1,27 @@ +export class dropGroup1676434944993 { + name = 'dropGroup1676434944993' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb"`); + await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_8fe87814e978053a53b1beb7e98"`); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "userGroupJoiningId"`); + await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "userGroupInvitationId"`); + await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."antenna_src_enum" AS ENUM('home', 'all', 'users', 'list')`); + await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "public"."antenna_src_enum" USING "src"::"text"::"public"."antenna_src_enum"`); + await queryRunner.query(`DROP TYPE "public"."antenna_src_enum_old"`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "emailNotificationTypes" SET DEFAULT '["follow","receiveFollowRequest"]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "emailNotificationTypes" SET DEFAULT '["follow", "receiveFollowRequest", "groupInvited"]'`); + await queryRunner.query(`CREATE TYPE "public"."antenna_src_enum_old" AS ENUM('home', 'all', 'users', 'list', 'group')`); + await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "public"."antenna_src_enum_old" USING "src"::"text"::"public"."antenna_src_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."antenna_src_enum"`); + await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum_old" RENAME TO "antenna_src_enum"`); + await queryRunner.query(`ALTER TABLE "notification" ADD "userGroupInvitationId" character varying(32)`); + await queryRunner.query(`ALTER TABLE "antenna" ADD "userGroupJoiningId" character varying(32)`); + await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_8fe87814e978053a53b1beb7e98" FOREIGN KEY ("userGroupInvitationId") REFERENCES "user_group_invitation"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb" FOREIGN KEY ("userGroupJoiningId") REFERENCES "user_group_joining"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/migration/1676438468213-ad3.js b/packages/backend/migration/1676438468213-ad3.js new file mode 100644 index 0000000000..18f56e8d36 --- /dev/null +++ b/packages/backend/migration/1676438468213-ad3.js @@ -0,0 +1,9 @@ +export class ad1676438468213 { + name = 'ad1676438468213'; + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "ad" ADD "startsAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + } + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "startsAt"`); + } +} diff --git a/packages/backend/migration/1677570181236-role-assignment-expires-at.js b/packages/backend/migration/1677570181236-role-assignment-expires-at.js new file mode 100644 index 0000000000..3ac2edab0a --- /dev/null +++ b/packages/backend/migration/1677570181236-role-assignment-expires-at.js @@ -0,0 +1,13 @@ +export class roleAssignmentExpiresAt1677570181236 { + name = 'roleAssignmentExpiresAt1677570181236' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "role_assignment" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`CREATE INDEX "IDX_539b6c08c05067599743bb6389" ON "role_assignment" ("expiresAt") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_539b6c08c05067599743bb6389"`); + await queryRunner.query(`ALTER TABLE "role_assignment" DROP COLUMN "expiresAt"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 6ec2ef4b76..42efb881e2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -7,46 +7,62 @@ "start": "node ./built/index.js", "start:test": "NODE_ENV=test node ./built/index.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js", - "build:swc": "swc src -d built -D", + "check:connect": "node ./check_connect.js", + "build": "swc src -d built -D", "watch:swc": "swc src -d built -D -w", - "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", + "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", - "lint": "tsc --noEmit && eslint --quiet \"src/**/*.ts\"", - "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --runInBand", - "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand", + "typecheck": "tsc --noEmit", + "eslint": "eslint --quiet \"src/**/*.ts\"", + "lint": "pnpm typecheck && pnpm eslint", + "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit", + "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" }, "optionalDependencies": { + "@swc/core-android-arm64": "^1.3.11", + "@swc/core-darwin-arm64": "^1.3.36", + "@swc/core-darwin-x64": "^1.3.36", + "@swc/core-linux-arm-gnueabihf": "^1.3.36", + "@swc/core-linux-arm64-gnu": "^1.3.36", + "@swc/core-linux-arm64-musl": "^1.3.36", + "@swc/core-linux-x64-gnu": "^1.3.36", + "@swc/core-linux-x64-musl": "^1.3.36", + "@swc/core-win32-arm64-msvc": "^1.3.36", + "@swc/core-win32-ia32-msvc": "^1.3.36", + "@swc/core-win32-x64-msvc": "^1.3.36", "@tensorflow/tfjs": "4.2.0", "@tensorflow/tfjs-node": "4.2.0" }, "dependencies": { - "@bull-board/api": "4.11.1", - "@bull-board/fastify": "4.11.1", - "@bull-board/ui": "4.11.1", + "@bull-board/api": "4.12.1", + "@bull-board/fastify": "4.12.1", + "@bull-board/ui": "4.12.1", "@discordapp/twemoji": "14.0.2", "@fastify/accepts": "4.1.0", "@fastify/cookie": "8.3.0", "@fastify/cors": "8.2.0", "@fastify/http-proxy": "8.4.0", - "@fastify/multipart": "7.4.0", - "@fastify/static": "6.8.0", + "@fastify/multipart": "7.4.1", + "@fastify/static": "6.9.0", "@fastify/view": "7.4.1", - "@nestjs/common": "9.3.7", - "@nestjs/core": "9.3.7", - "@nestjs/testing": "9.3.7", + "@nestjs/common": "9.3.9", + "@nestjs/core": "9.3.9", + "@nestjs/testing": "9.3.9", "@peertube/http-signature": "1.7.0", "@sinonjs/fake-timers": "10.0.2", + "@swc/cli": "0.1.62", + "@swc/core": "1.3.36", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "5.3.1", "autwh": "0.1.0", - "aws-sdk": "2.1295.0", + "aws-sdk": "2.1318.0", "bcryptjs": "2.4.3", - "blurhash": "2.0.4", - "bull": "4.10.3", + "blurhash": "2.0.5", + "bull": "4.10.4", "cacheable-lookup": "6.1.0", "cbor": "8.1.0", "chalk": "5.2.0", @@ -58,12 +74,13 @@ "date-fns": "2.29.3", "deep-email-validator": "0.1.21", "escape-regexp": "0.0.1", - "fastify": "4.12.0", + "fastify": "4.13.0", "feed": "4.2.2", - "file-type": "18.2.0", + "file-type": "18.2.1", "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", "got": "12.5.3", + "happy-dom": "8.9.0", "hpagent": "1.2.0", "ioredis": "4.28.5", "ip-cidr": "3.1.0", @@ -71,7 +88,7 @@ "js-yaml": "4.1.0", "jsdom": "21.1.0", "json5": "2.2.3", - "jsonld": "8.1.0", + "jsonld": "8.1.1", "jsrsasign": "10.6.1", "mfm-js": "0.23.3", "mime-types": "2.1.35", @@ -83,6 +100,7 @@ "nsfwjs": "2.4.2", "oauth": "0.10.0", "os-utils": "0.0.14", + "otpauth": "^9.0.2", "parse5": "7.1.2", "pg": "8.9.0", "private-ip": "3.0.0", @@ -102,21 +120,20 @@ "rss-parser": "3.12.0", "rxjs": "7.8.0", "s-age": "1.1.2", - "sanitize-html": "2.9.0", + "sanitize-html": "2.10.0", "seedrandom": "3.0.5", "semver": "7.3.8", "sharp": "0.31.3", - "speakeasy": "2.0.0", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "summaly": "2.7.0", - "systeminformation": "5.17.8", + "summaly": "github:misskey-dev/summaly", + "systeminformation": "5.17.10", "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.2", "tsconfig-paths": "4.1.2", "twemoji-parser": "14.0.0", - "typeorm": "0.3.12", + "typeorm": "0.3.11", "typescript": "4.9.5", "ulid": "2.3.0", "unzipper": "0.10.11", @@ -124,14 +141,11 @@ "vary": "1.1.2", "web-push": "3.5.0", "websocket": "1.0.34", - "ws": "8.12.0", + "ws": "8.12.1", "xev": "3.0.2" }, "devDependencies": { - "@jest/globals": "29.4.2", - "@redocly/openapi-core": "1.0.0-beta.123", - "@swc/cli": "0.1.61", - "@swc/core": "1.3.34", + "@jest/globals": "29.4.3", "@swc/jest": "0.2.24", "@types/accepts": "1.3.5", "@types/archiver": "5.3.1", @@ -141,7 +155,7 @@ "@types/color-convert": "2.0.0", "@types/content-disposition": "0.5.5", "@types/escape-regexp": "0.0.1", - "@types/fluent-ffmpeg": "2.1.20", + "@types/fluent-ffmpeg": "2.1.21", "@types/ioredis": "4.28.10", "@types/jest": "29.4.0", "@types/js-yaml": "4.0.5", @@ -149,7 +163,7 @@ "@types/jsonld": "1.5.8", "@types/jsrsasign": "10.5.5", "@types/mime-types": "2.1.1", - "@types/node": "18.13.0", + "@types/node": "18.14.1", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.7", "@types/oauth": "0.9.1", @@ -165,22 +179,21 @@ "@types/semver": "7.3.13", "@types/sharp": "0.31.1", "@types/sinonjs__fake-timers": "8.1.2", - "@types/speakeasy": "2.0.7", "@types/tinycolor2": "1.4.3", "@types/tmp": "0.2.3", "@types/unzipper": "0.10.5", - "@types/uuid": "9.0.0", + "@types/uuid": "9.0.1", "@types/vary": "1.1.0", "@types/web-push": "3.3.2", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.51.0", - "@typescript-eslint/parser": "5.51.0", + "@typescript-eslint/eslint-plugin": "5.52.0", + "@typescript-eslint/parser": "5.53.0", "cross-env": "7.0.3", - "eslint": "8.33.0", + "eslint": "8.35.0", "eslint-plugin-import": "2.27.5", "execa": "6.1.0", - "jest": "29.4.2", - "jest-mock": "29.4.2" + "jest": "29.4.3", + "jest-mock": "29.4.3" } } diff --git a/packages/backend/src/@types/redis-lock.d.ts b/packages/backend/src/@types/redis-lock.d.ts new file mode 100644 index 0000000000..9242656a98 --- /dev/null +++ b/packages/backend/src/@types/redis-lock.d.ts @@ -0,0 +1,8 @@ +declare module 'redis-lock' { + import type Redis from 'ioredis'; + + type Lock = (lockName: string, timeout?: number, taskToPerform?: () => Promise) => void; + function redisLock(client: Redis.Redis, retryDelay: number): Lock; + + export = redisLock; +} diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 35416209a0..801f1db741 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -1,3 +1,4 @@ +import { setTimeout } from 'node:timers/promises'; import { Global, Inject, Module } from '@nestjs/common'; import Redis from 'ioredis'; import { DataSource } from 'typeorm'; @@ -57,6 +58,14 @@ export class GlobalModule implements OnApplicationShutdown { ) {} async onApplicationShutdown(signal: string): Promise { + if (process.env.NODE_ENV === 'test') { + // XXX: + // Shutting down the existing connections causes errors on Jest as + // Misskey has asynchronous postgres/redis connections that are not + // awaited. + // Let's wait for some random time for them to finish. + await setTimeout(5000); + } await Promise.all([ this.db.destroy(), this.redisClient.disconnect(), diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts index 04aa26e652..279a1fe59d 100644 --- a/packages/backend/src/boot/common.ts +++ b/packages/backend/src/boot/common.ts @@ -16,12 +16,14 @@ export async function server() { app.enableShutdownHooks(); const serverService = app.get(ServerService); - serverService.launch(); + await serverService.launch(); app.get(ChartManagementService).start(); app.get(JanitorService).start(); app.get(QueueStatsService).start(); app.get(ServerStatsService).start(); + + return app; } export async function jobQueue() { diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 4309f67cb3..dec226c42d 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -69,6 +69,7 @@ export type Source = { mediaProxy?: string; proxyRemoteFiles?: boolean; + videoThumbnailGenerator?: string; signToActivityPubGet?: boolean; }; @@ -91,6 +92,7 @@ export type Mixin = { clientManifestExists: boolean; mediaProxy: string; externalMediaProxyEnabled: boolean; + videoThumbnailGenerator: string | null; }; export type Config = Source & Mixin; @@ -146,6 +148,10 @@ export function loadConfig() { mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy; mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy; + mixin.videoThumbnailGenerator = config.videoThumbnailGenerator ? + config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator + : null; + if (!config.redis.prefix) config.redis.prefix = mixin.host; return Object.assign(config, mixin); diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 5f6dfca0ca..d8ba7b169d 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -32,7 +32,7 @@ export class AccountUpdateService { // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 if (this.userEntityService.isLocalUser(user)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); + const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); } diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index a71327e947..05930350fa 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js'; import * as Acct from '@/misc/acct.js'; import type { Packed } from '@/misc/schema.js'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @@ -39,9 +39,6 @@ export class AntennaService implements OnApplicationShutdown { @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - @Inject(DI.userListJoiningsRepository) private userListJoiningsRepository: UserListJoiningsRepository, @@ -160,14 +157,6 @@ export class AntennaService implements OnApplicationShutdown { })).map(x => x.userId); if (!listUsers.includes(note.userId)) return false; - } else if (antenna.src === 'group') { - const joining = await this.userGroupJoiningsRepository.findOneByOrFail({ id: antenna.userGroupJoiningId! }); - - const groupUsers = (await this.userGroupJoiningsRepository.findBy({ - userGroupId: joining.userGroupId, - })).map(x => x.userId); - - if (!groupUsers.includes(note.userId)) return false; } else if (antenna.src === 'users') { const accts = antenna.users.map(x => { const { username, host } = Acct.parse(x); @@ -182,13 +171,15 @@ export class AntennaService implements OnApplicationShutdown { .filter(xs => xs.length > 0); if (keywords.length > 0) { - if (note.text == null) return false; + if (note.text == null && note.cw == null) return false; + + const _text = (note.text ?? '') + '\n' + (note.cw ?? ''); const matched = keywords.some(and => and.every(keyword => antenna.caseSensitive - ? note.text!.includes(keyword) - : note.text!.toLowerCase().includes(keyword.toLowerCase()), + ? _text.includes(keyword) + : _text.toLowerCase().includes(keyword.toLowerCase()), )); if (!matched) return false; @@ -200,13 +191,15 @@ export class AntennaService implements OnApplicationShutdown { .filter(xs => xs.length > 0); if (excludeKeywords.length > 0) { - if (note.text == null) return false; - + if (note.text == null && note.cw == null) return false; + + const _text = (note.text ?? '') + '\n' + (note.cw ?? ''); + const matched = excludeKeywords.some(and => and.every(keyword => antenna.caseSensitive - ? note.text!.includes(keyword) - : note.text!.toLowerCase().includes(keyword.toLowerCase()), + ? _text.includes(keyword) + : _text.toLowerCase().includes(keyword.toLowerCase()), )); if (matched) return false; diff --git a/packages/backend/src/core/AppLockService.ts b/packages/backend/src/core/AppLockService.ts index 5f3072a415..ee179b7f01 100644 --- a/packages/backend/src/core/AppLockService.ts +++ b/packages/backend/src/core/AppLockService.ts @@ -12,7 +12,7 @@ const retryDelay = 100; @Injectable() export class AppLockService { - private lock: (key: string, timeout?: number) => Promise<() => void>; + private lock: (key: string, timeout?: number, _?: (() => Promise) | undefined) => Promise<() => void>; constructor( @Inject(DI.redis) diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 6a6d1b864a..491d8ab113 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -1,5 +1,4 @@ import { Module } from '@nestjs/common'; -import { DI } from '../di-symbols.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; import { AntennaService } from './AntennaService.js'; @@ -22,7 +21,6 @@ import { IdService } from './IdService.js'; import { ImageProcessingService } from './ImageProcessingService.js'; import { InstanceActorService } from './InstanceActorService.js'; import { InternalStorageService } from './InternalStorageService.js'; -import { MessagingService } from './MessagingService.js'; import { MetaService } from './MetaService.js'; import { MfmService } from './MfmService.js'; import { ModerationLogService } from './ModerationLogService.js'; @@ -82,7 +80,6 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js'; import { HashtagEntityService } from './entities/HashtagEntityService.js'; import { InstanceEntityService } from './entities/InstanceEntityService.js'; -import { MessagingMessageEntityService } from './entities/MessagingMessageEntityService.js'; import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js'; import { MutingEntityService } from './entities/MutingEntityService.js'; import { NoteEntityService } from './entities/NoteEntityService.js'; @@ -93,8 +90,6 @@ import { PageEntityService } from './entities/PageEntityService.js'; import { PageLikeEntityService } from './entities/PageLikeEntityService.js'; import { SigninEntityService } from './entities/SigninEntityService.js'; import { UserEntityService } from './entities/UserEntityService.js'; -import { UserGroupEntityService } from './entities/UserGroupEntityService.js'; -import { UserGroupInvitationEntityService } from './entities/UserGroupInvitationEntityService.js'; import { UserListEntityService } from './entities/UserListEntityService.js'; import { FlashEntityService } from './entities/FlashEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; @@ -146,7 +141,6 @@ const $IdService: Provider = { provide: 'IdService', useExisting: IdService }; const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService }; const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService }; const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService }; -const $MessagingService: Provider = { provide: 'MessagingService', useExisting: MessagingService }; const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService }; const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService }; const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService }; @@ -207,7 +201,6 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService }; const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService }; const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService }; -const $MessagingMessageEntityService: Provider = { provide: 'MessagingMessageEntityService', useExisting: MessagingMessageEntityService }; const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService }; const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService }; const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService }; @@ -218,8 +211,6 @@ const $PageEntityService: Provider = { provide: 'PageEntityService', useExisting const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', useExisting: PageLikeEntityService }; const $SigninEntityService: Provider = { provide: 'SigninEntityService', useExisting: SigninEntityService }; const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting: UserEntityService }; -const $UserGroupEntityService: Provider = { provide: 'UserGroupEntityService', useExisting: UserGroupEntityService }; -const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitationEntityService', useExisting: UserGroupInvitationEntityService }; const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService }; const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; @@ -273,7 +264,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ImageProcessingService, InstanceActorService, InternalStorageService, - MessagingService, MetaService, MfmService, ModerationLogService, @@ -333,7 +323,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting GalleryPostEntityService, HashtagEntityService, InstanceEntityService, - MessagingMessageEntityService, ModerationLogEntityService, MutingEntityService, NoteEntityService, @@ -344,8 +333,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PageLikeEntityService, SigninEntityService, UserEntityService, - UserGroupEntityService, - UserGroupInvitationEntityService, UserListEntityService, FlashEntityService, FlashLikeEntityService, @@ -394,7 +381,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ImageProcessingService, $InstanceActorService, $InternalStorageService, - $MessagingService, $MetaService, $MfmService, $ModerationLogService, @@ -454,7 +440,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $GalleryPostEntityService, $HashtagEntityService, $InstanceEntityService, - $MessagingMessageEntityService, $ModerationLogEntityService, $MutingEntityService, $NoteEntityService, @@ -465,8 +450,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PageLikeEntityService, $SigninEntityService, $UserEntityService, - $UserGroupEntityService, - $UserGroupInvitationEntityService, $UserListEntityService, $FlashEntityService, $FlashLikeEntityService, @@ -516,7 +499,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ImageProcessingService, InstanceActorService, InternalStorageService, - MessagingService, MetaService, MfmService, ModerationLogService, @@ -575,7 +557,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting GalleryPostEntityService, HashtagEntityService, InstanceEntityService, - MessagingMessageEntityService, ModerationLogEntityService, MutingEntityService, NoteEntityService, @@ -586,8 +567,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PageLikeEntityService, SigninEntityService, UserEntityService, - UserGroupEntityService, - UserGroupInvitationEntityService, UserListEntityService, FlashEntityService, FlashLikeEntityService, @@ -636,7 +615,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ImageProcessingService, $InstanceActorService, $InternalStorageService, - $MessagingService, $MetaService, $MfmService, $ModerationLogService, @@ -695,7 +673,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $GalleryPostEntityService, $HashtagEntityService, $InstanceEntityService, - $MessagingMessageEntityService, $ModerationLogEntityService, $MutingEntityService, $NoteEntityService, @@ -706,8 +683,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PageLikeEntityService, $SigninEntityService, $UserEntityService, - $UserGroupEntityService, - $UserGroupInvitationEntityService, $UserListEntityService, $FlashEntityService, $FlashLikeEntityService, diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts index cd47844a75..eba7171fb6 100644 --- a/packages/backend/src/core/CreateNotificationService.ts +++ b/packages/backend/src/core/CreateNotificationService.ts @@ -1,4 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { setTimeout } from 'node:timers/promises'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { Notification } from '@/models/entities/Notification.js'; @@ -10,7 +11,9 @@ import { PushNotificationService } from '@/core/PushNotificationService.js'; import { bindThis } from '@/decorators.js'; @Injectable() -export class CreateNotificationService { +export class CreateNotificationService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -40,11 +43,11 @@ export class CreateNotificationService { if (data.notifierId && (notifieeId === data.notifierId)) { return null; } - + const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); - + const isMuted = profile?.mutingNotificationTypes.includes(type); - + // Create notification const notification = await this.notificationsRepository.insert({ id: this.idService.genId(), @@ -56,18 +59,18 @@ export class CreateNotificationService { ...data, } as Partial) .then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0])); - + const packed = await this.notificationEntityService.pack(notification, {}); - + // Publish notification event this.globalEventService.publishMainStream(notifieeId, 'notification', packed); - + // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する - setTimeout(async () => { + setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { const fresh = await this.notificationsRepository.findOneBy({ id: notification.id }); if (fresh == null) return; // 既に削除されているかもしれない if (fresh.isRead) return; - + //#region ただしミュートしているユーザーからの通知なら無視 const mutings = await this.mutingsRepository.findBy({ muterId: notifieeId, @@ -76,14 +79,14 @@ export class CreateNotificationService { return; } //#endregion - + this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); - + if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); - }, 2000); - + }, () => { /* aborted, ignore it */ }); + return notification; } @@ -103,7 +106,7 @@ export class CreateNotificationService { sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); */ } - + @bindThis private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { /* @@ -115,4 +118,8 @@ export class CreateNotificationService { sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); */ } + + onApplicationShutdown(signal?: string | undefined): void { + this.#shutdownController.abort(); + } } diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 63f0319442..a1a257fbd1 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -61,7 +61,7 @@ export class CustomEmojiService { await this.db.queryResultCache!.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiAdded', { - emoji: await this.emojiEntityService.pack(emoji.id), + emoji: await this.emojiEntityService.packDetailed(emoji.id), }); } diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 598a457e83..b15c967c85 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -7,7 +7,7 @@ import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import Logger from '@/logger.js'; -import type { IRemoteUser, User } from '@/models/entities/User.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveFile } from '@/models/entities/DriveFile.js'; import { IdService } from '@/core/IdService.js'; @@ -250,6 +250,14 @@ export class DriveService { @bindThis public async generateAlts(path: string, type: string, generateWeb: boolean) { if (type.startsWith('video/')) { + if (this.config.videoThumbnailGenerator != null) { + // videoThumbnailGeneratorが指定されていたら動画サムネイル生成はスキップ + return { + webpublic: null, + thumbnail: null, + }; + } + try { const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path); return { @@ -391,7 +399,7 @@ export class DriveService { } @bindThis - private async deleteOldFile(user: IRemoteUser) { + private async deleteOldFile(user: RemoteUser) { const q = this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId', { userId: user.id }) .andWhere('file.isLink = FALSE'); @@ -492,7 +500,7 @@ export class DriveService { throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.'); } else { // (アバターまたはバナーを含まず)最も古いファイルを削除する - this.deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as IRemoteUser); + this.deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser); } } } diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 35f30deeb4..bbc8b4332e 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -2,7 +2,6 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { JSDOM } from 'jsdom'; import tinycolor from 'tinycolor2'; -import fetch from 'node-fetch'; import type { Instance } from '@/models/entities/Instance.js'; import type { InstancesRepository } from '@/models/index.js'; import { AppLockService } from '@/core/AppLockService.js'; diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index 67337b5056..e39b134b7e 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -3,7 +3,7 @@ import * as crypto from 'node:crypto'; import { join } from 'node:path'; import * as stream from 'node:stream'; import * as util from 'node:util'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { FSWatcher } from 'chokidar'; import { fileTypeFromFile } from 'file-type'; import FFmpeg from 'fluent-ffmpeg'; diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 784612149d..65a69a0235 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -3,21 +3,15 @@ import Redis from 'ioredis'; import type { User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import type { UserList } from '@/models/entities/UserList.js'; -import type { UserGroup } from '@/models/entities/UserGroup.js'; import type { Antenna } from '@/models/entities/Antenna.js'; -import type { Channel } from '@/models/entities/Channel.js'; import type { StreamChannels, AdminStreamTypes, AntennaStreamTypes, BroadcastTypes, - ChannelStreamTypes, DriveStreamTypes, - GroupMessagingStreamTypes, InternalStreamTypes, MainStreamTypes, - MessagingIndexStreamTypes, - MessagingStreamTypes, NoteStreamTypes, UserListStreamTypes, UserStreamTypes, @@ -83,11 +77,6 @@ export class GlobalEventService { }); } - @bindThis - public publishChannelStream(channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void { - this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value); - } - @bindThis public publishUserListStream(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void { this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); @@ -98,21 +87,6 @@ export class GlobalEventService { this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); } - @bindThis - public publishMessagingStream(userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void { - this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); - } - - @bindThis - public publishGroupMessagingStream(groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void { - this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value); - } - - @bindThis - public publishMessagingIndexStream(userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void { - this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); - } - @bindThis public publishNotesStream(note: Packed<'Note'>): void { this.publish('notesStream', null, note); diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index e32026b04f..375aa846cb 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -99,7 +99,6 @@ export class HttpRequestService { const res = await this.send(url, { method: 'GET', headers: Object.assign({ - 'User-Agent': this.config.userAgent, Accept: accept, }, headers ?? {}), timeout: 5000, @@ -114,7 +113,6 @@ export class HttpRequestService { const res = await this.send(url, { method: 'GET', headers: Object.assign({ - 'User-Agent': this.config.userAgent, Accept: accept, }, headers ?? {}), timeout: 5000, @@ -144,7 +142,10 @@ export class HttpRequestService { const res = await fetch(url, { method: args.method ?? 'GET', - headers: args.headers, + headers: { + 'User-Agent': this.config.userAgent, + ...(args.headers ?? {}) + }, body: args.body, size: args.size ?? 10 * 1024 * 1024, agent: (url) => this.getAgentByUrl(url), diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index fbc02f504b..7c88f5e9a0 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -107,7 +107,7 @@ export class ImageProcessingService { withoutEnlargement: true, }) .rotate() - .webp(options) + .webp(options); return { data, diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index 0b4a83c634..ee9ae0733f 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; -import type { ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import type { UsersRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; import { DI } from '@/di-symbols.js'; @@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const; @Injectable() export class InstanceActorService { - private cache: Cache; + private cache: Cache; constructor( @Inject(DI.usersRepository) @@ -19,24 +19,24 @@ export class InstanceActorService { private createSystemUserService: CreateSystemUserService, ) { - this.cache = new Cache(Infinity); + this.cache = new Cache(Infinity); } @bindThis - public async getInstanceActor(): Promise { + public async getInstanceActor(): Promise { const cached = this.cache.get(null); if (cached) return cached; const user = await this.usersRepository.findOneBy({ host: IsNull(), username: ACTOR_USERNAME, - }) as ILocalUser | undefined; + }) as LocalUser | undefined; if (user) { this.cache.set(null, user); return user; } else { - const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as ILocalUser; + const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser; this.cache.set(null, created); return created; } diff --git a/packages/backend/src/core/MessagingService.ts b/packages/backend/src/core/MessagingService.ts deleted file mode 100644 index f4a1090658..0000000000 --- a/packages/backend/src/core/MessagingService.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { In, Not } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; -import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; -import type { Note } from '@/models/entities/Note.js'; -import type { User, CacheableUser, IRemoteUser } from '@/models/entities/User.js'; -import type { UserGroup } from '@/models/entities/UserGroup.js'; -import { QueueService } from '@/core/QueueService.js'; -import { toArray } from '@/misc/prelude/array.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { MessagingMessagesRepository, MutingsRepository, UserGroupJoiningsRepository, UsersRepository } from '@/models/index.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; -import { bindThis } from '@/decorators.js'; - -@Injectable() -export class MessagingService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - private userEntityService: UserEntityService, - private messagingMessageEntityService: MessagingMessageEntityService, - private idService: IdService, - private globalEventService: GlobalEventService, - private apRendererService: ApRendererService, - private queueService: QueueService, - private pushNotificationService: PushNotificationService, - ) { - } - - @bindThis - public async createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: CacheableUser | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { - const message = { - id: this.idService.genId(), - createdAt: new Date(), - fileId: file ? file.id : null, - recipientId: recipientUser ? recipientUser.id : null, - groupId: recipientGroup ? recipientGroup.id : null, - text: text ? text.trim() : null, - userId: user.id, - isRead: false, - reads: [] as any[], - uri, - } as MessagingMessage; - - await this.messagingMessagesRepository.insert(message); - - const messageObj = await this.messagingMessageEntityService.pack(message); - - if (recipientUser) { - if (this.userEntityService.isLocalUser(user)) { - // 自分のストリーム - this.globalEventService.publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj); - this.globalEventService.publishMessagingIndexStream(message.userId, 'message', messageObj); - this.globalEventService.publishMainStream(message.userId, 'messagingMessage', messageObj); - } - - if (this.userEntityService.isLocalUser(recipientUser)) { - // 相手のストリーム - this.globalEventService.publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj); - this.globalEventService.publishMessagingIndexStream(recipientUser.id, 'message', messageObj); - this.globalEventService.publishMainStream(recipientUser.id, 'messagingMessage', messageObj); - } - } else if (recipientGroup) { - // グループのストリーム - this.globalEventService.publishGroupMessagingStream(recipientGroup.id, 'message', messageObj); - - // メンバーのストリーム - const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: recipientGroup.id }); - for (const joining of joinings) { - this.globalEventService.publishMessagingIndexStream(joining.userId, 'message', messageObj); - this.globalEventService.publishMainStream(joining.userId, 'messagingMessage', messageObj); - } - } - - // 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する - setTimeout(async () => { - const freshMessage = await this.messagingMessagesRepository.findOneBy({ id: message.id }); - if (freshMessage == null) return; // メッセージが削除されている場合もある - - if (recipientUser && this.userEntityService.isLocalUser(recipientUser)) { - if (freshMessage.isRead) return; // 既読 - - //#region ただしミュートされているなら発行しない - const mute = await this.mutingsRepository.findBy({ - muterId: recipientUser.id, - }); - if (mute.map(m => m.muteeId).includes(user.id)) return; - //#endregion - - this.globalEventService.publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj); - this.pushNotificationService.pushNotification(recipientUser.id, 'unreadMessagingMessage', messageObj); - } else if (recipientGroup) { - const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: recipientGroup.id, userId: Not(user.id) }); - for (const joining of joinings) { - if (freshMessage.reads.includes(joining.userId)) return; // 既読 - this.globalEventService.publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj); - this.pushNotificationService.pushNotification(joining.userId, 'unreadMessagingMessage', messageObj); - } - } - }, 2000); - - if (recipientUser && this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipientUser)) { - const note = { - id: message.id, - createdAt: message.createdAt, - fileIds: message.fileId ? [message.fileId] : [], - text: message.text, - userId: message.userId, - visibility: 'specified', - mentions: [recipientUser].map(u => u.id), - mentionedRemoteUsers: JSON.stringify([recipientUser].map(u => ({ - uri: u.uri, - username: u.username, - host: u.host, - }))), - } as Note; - - const activity = this.apRendererService.renderActivity(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note)); - - this.queueService.deliver(user, activity, recipientUser.inbox); - } - return messageObj; - } - - @bindThis - public async deleteMessage(message: MessagingMessage) { - await this.messagingMessagesRepository.delete(message.id); - this.postDeleteMessage(message); - } - - @bindThis - private async postDeleteMessage(message: MessagingMessage) { - if (message.recipientId) { - const user = await this.usersRepository.findOneByOrFail({ id: message.userId }); - const recipient = await this.usersRepository.findOneByOrFail({ id: message.recipientId }); - - if (this.userEntityService.isLocalUser(user)) this.globalEventService.publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); - if (this.userEntityService.isLocalUser(recipient)) this.globalEventService.publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); - - if (this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipient)) { - const activity = this.apRendererService.renderActivity(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), user)); - this.queueService.deliver(user, activity, recipient.inbox); - } - } else if (message.groupId) { - this.globalEventService.publishGroupMessagingStream(message.groupId, 'deleted', message.id); - } - } - - /** - * Mark messages as read - */ - @bindThis - public async readUserMessagingMessage( - userId: User['id'], - otherpartyId: User['id'], - messageIds: MessagingMessage['id'][], - ) { - if (messageIds.length === 0) return; - - const messages = await this.messagingMessagesRepository.findBy({ - id: In(messageIds), - }); - - for (const message of messages) { - if (message.recipientId !== userId) { - throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).'); - } - } - - // Update documents - await this.messagingMessagesRepository.update({ - id: In(messageIds), - userId: otherpartyId, - recipientId: userId, - isRead: false, - }, { - isRead: true, - }); - - // Publish event - this.globalEventService.publishMessagingStream(otherpartyId, userId, 'read', messageIds); - this.globalEventService.publishMessagingIndexStream(userId, 'read', messageIds); - - if (!await this.userEntityService.getHasUnreadMessagingMessage(userId)) { - // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 - this.globalEventService.publishMainStream(userId, 'readAllMessagingMessages'); - this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessages', undefined); - } else { - // そのユーザーとのメッセージで未読がなければイベント発行 - const count = await this.messagingMessagesRepository.count({ - where: { - userId: otherpartyId, - recipientId: userId, - isRead: false, - }, - take: 1, - }); - - if (!count) { - this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId }); - } - } - } - - /** - * Mark messages as read - */ - @bindThis - public async readGroupMessagingMessage( - userId: User['id'], - groupId: UserGroup['id'], - messageIds: MessagingMessage['id'][], - ) { - if (messageIds.length === 0) return; - - // check joined - const joining = await this.userGroupJoiningsRepository.findOneBy({ - userId: userId, - userGroupId: groupId, - }); - - if (joining == null) { - throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).'); - } - - const messages = await this.messagingMessagesRepository.findBy({ - id: In(messageIds), - }); - - const reads: MessagingMessage['id'][] = []; - - for (const message of messages) { - if (message.userId === userId) continue; - if (message.reads.includes(userId)) continue; - - // Update document - await this.messagingMessagesRepository.createQueryBuilder().update() - .set({ - reads: (() => `array_append("reads", '${joining.userId}')`) as any, - }) - .where('id = :id', { id: message.id }) - .execute(); - - reads.push(message.id); - } - - // Publish event - this.globalEventService.publishGroupMessagingStream(groupId, 'read', { - ids: reads, - userId: userId, - }); - this.globalEventService.publishMessagingIndexStream(userId, 'read', reads); - - if (!await this.userEntityService.getHasUnreadMessagingMessage(userId)) { - // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 - this.globalEventService.publishMainStream(userId, 'readAllMessagingMessages'); - this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessages', undefined); - } else { - // そのグループにおいて未読がなければイベント発行 - const unreadExist = await this.messagingMessagesRepository.createQueryBuilder('message') - .where('message.groupId = :groupId', { groupId: groupId }) - .andWhere('message.userId != :userId', { userId: userId }) - .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) - .andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない - .getOne().then(x => x != null); - - if (!unreadExist) { - this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId }); - } - } - } - - @bindThis - public async deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) { - messages = toArray(messages).filter(x => x.uri); - const contents = messages.map(x => this.apRendererService.renderRead(user, x)); - - if (contents.length > 1) { - const collection = this.apRendererService.renderOrderedCollection(null, contents.length, undefined, undefined, contents); - this.queueService.deliver(user, this.apRendererService.renderActivity(collection), recipient.inbox); - } else { - for (const content of contents) { - this.queueService.deliver(user, this.apRendererService.renderActivity(content), recipient.inbox); - } - } - } -} diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 6c40ba25a1..9b2d5dc0ff 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -1,9 +1,8 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import * as parse5 from 'parse5'; -import { JSDOM } from 'jsdom'; +import { Window } from 'happy-dom'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; import type { IMentionedRemoteUsers } from '@/models/entities/Note.js'; @@ -236,7 +235,7 @@ export class MfmService { return null; } - const { window } = new JSDOM(''); + const { window } = new Window(); const doc = window.document; @@ -301,7 +300,7 @@ export class MfmService { hashtag: (node) => { const a = doc.createElement('a'); - a.href = `${this.config.url}/tags/${node.props.hashtag}`; + a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`); a.textContent = `#${node.props.hashtag}`; a.setAttribute('rel', 'tag'); return a; @@ -327,7 +326,7 @@ export class MfmService { link: (node) => { const a = doc.createElement('a'); - a.href = node.props.url; + a.setAttribute('href', node.props.url); appendChildren(node.children, a); return a; }, @@ -336,7 +335,7 @@ export class MfmService { const a = doc.createElement('a'); const { username, host, acct } = node.props; const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); - a.href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`; + a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`); a.className = 'u-url mention'; a.textContent = acct; return a; @@ -361,14 +360,14 @@ export class MfmService { url: (node) => { const a = doc.createElement('a'); - a.href = node.props.url; + a.setAttribute('href', node.props.url); a.textContent = node.props.url; return a; }, search: (node) => { const a = doc.createElement('a'); - a.href = `https://www.google.com/search?q=${node.props.query}`; + a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`); a.textContent = node.props.content; return a; }, diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 4a81f764dc..4c4261ba79 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -1,6 +1,7 @@ +import { setImmediate } from 'node:timers/promises'; import * as mfm from 'mfm-js'; -import { Not, In, DataSource } from 'typeorm'; -import { Inject, Injectable } from '@nestjs/common'; +import { In, DataSource } from 'typeorm'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; @@ -11,7 +12,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { App } from '@/models/entities/App.js'; import { concat } from '@/misc/prelude/array.js'; import { IdService } from '@/core/IdService.js'; -import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; +import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { IPoll } from '@/models/entities/Poll.js'; import { Poll } from '@/models/entities/Poll.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; @@ -52,7 +53,7 @@ class NotificationManager { private notifier: { id: User['id']; }; private note: Note; private queue: { - target: ILocalUser['id']; + target: LocalUser['id']; reason: NotificationType; }[]; @@ -68,7 +69,7 @@ class NotificationManager { } @bindThis - public push(notifiee: ILocalUser['id'], reason: NotificationType) { + public push(notifiee: LocalUser['id'], reason: NotificationType) { // 自分自身へは通知しない if (this.notifier.id === notifiee) return; @@ -137,7 +138,9 @@ type Option = { }; @Injectable() -export class NoteCreateService { +export class NoteCreateService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + constructor( @Inject(DI.config) private config: Config, @@ -313,7 +316,10 @@ export class NoteCreateService { const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); - setImmediate(() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!)); + setImmediate('post created', { signal: this.#shutdownController.signal }).then( + () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!), + () => { /* aborted, ignore this */ }, + ); return note; } @@ -605,7 +611,7 @@ export class NoteCreateService { // メンションされたリモートユーザーに配送 for (const u of mentionedUsers.filter(u => this.userEntityService.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); + dm.addDirectRecipe(u as RemoteUser); } // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 @@ -711,7 +717,7 @@ export class NoteCreateService { ? 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); - return this.apRendererService.renderActivity(content); + return this.apRendererService.addContext(content); } @bindThis @@ -756,4 +762,8 @@ export class NoteCreateService { return mentionedUsers; } + + onApplicationShutdown(signal?: string | undefined) { + this.#shutdownController.abort(); + } } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 4dad825097..571b625523 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -1,6 +1,6 @@ import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; -import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; +import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { Note, IMentionedRemoteUsers } from '@/models/entities/Note.js'; import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js'; import { RelayService } from '@/core/RelayService.js'; @@ -78,7 +78,7 @@ export class NoteDeleteService { }); } - const content = this.apRendererService.renderActivity(renote + const content = this.apRendererService.addContext(renote ? this.apRendererService.renderUndo(this.apRendererService.renderAnnounce(renote.uri ?? `${this.config.url}/notes/${renote.id}`, note), user) : this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${note.id}`), user)); @@ -90,7 +90,7 @@ export class NoteDeleteService { for (const cascadingNote of cascadingNotes) { if (!cascadingNote.user) continue; if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; - const content = this.apRendererService.renderActivity(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); + const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); this.deliverToConcerned(cascadingNote.user, cascadingNote, content); } //#endregion @@ -159,11 +159,11 @@ export class NoteDeleteService { return await this.usersRepository.find({ where, - }) as IRemoteUser[]; + }) as RemoteUser[]; } @bindThis - private async deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) { + private async deliverToConcerned(user: { id: LocalUser['id']; host: null; }, note: Note, content: any) { this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); const remoteUsers = await this.getMentionedRemoteUsers(note); diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index bb6def1edb..3a9f832ac0 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -115,7 +115,7 @@ export class NotePiningService { const target = `${this.config.url}/users/${user.id}/collections/featured`; const item = `${this.config.url}/notes/${noteId}`; - const content = this.apRendererService.renderActivity(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item)); + const content = this.apRendererService.addContext(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item)); this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index 84983d600e..d23fb8238b 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -1,4 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { setTimeout } from 'node:timers/promises'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { In, IsNull, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { User } from '@/models/entities/User.js'; @@ -15,7 +16,9 @@ import { AntennaService } from './AntennaService.js'; import { PushNotificationService } from './PushNotificationService.js'; @Injectable() -export class NoteReadService { +export class NoteReadService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -60,14 +63,14 @@ export class NoteReadService { }); if (mute.map(m => m.muteeId).includes(note.userId)) return; //#endregion - + // スレッドミュート const threadMute = await this.noteThreadMutingsRepository.findOneBy({ userId: userId, threadId: note.threadId ?? note.id, }); if (threadMute) return; - + const unread = { id: this.idService.genId(), noteId: note.id, @@ -77,15 +80,15 @@ export class NoteReadService { noteChannelId: note.channelId, noteUserId: note.userId, }; - + await this.noteUnreadsRepository.insert(unread); - + // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する - setTimeout(async () => { + setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id }); - + if (exist == null) return; - + if (params.isMentioned) { this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); } @@ -95,8 +98,8 @@ export class NoteReadService { if (note.channelId) { this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id); } - }, 2000); - } + }, () => { /* aborted, ignore it */ }); + } @bindThis public async read( @@ -113,24 +116,24 @@ export class NoteReadService { }, select: ['followeeId'], })).map(x => x.followeeId)); - + const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); const readMentions: (Note | Packed<'Note'>)[] = []; const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; const readChannelNotes: (Note | Packed<'Note'>)[] = []; const readAntennaNotes: (Note | Packed<'Note'>)[] = []; - + for (const note of notes) { if (note.mentions && note.mentions.includes(userId)) { readMentions.push(note); } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { readSpecifiedNotes.push(note); } - + if (note.channelId && followingChannels.has(note.channelId)) { readChannelNotes.push(note); } - + if (note.user != null) { // たぶんnullになることは無いはずだけど一応 for (const antenna of myAntennas) { if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) { @@ -139,14 +142,14 @@ export class NoteReadService { } } } - + if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { // Remove the record await this.noteUnreadsRepository.delete({ userId: userId, noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]), }); - + // TODO: ↓まとめてクエリしたい this.noteUnreadsRepository.countBy({ @@ -183,7 +186,7 @@ export class NoteReadService { noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), }); } - + if (readAntennaNotes.length > 0) { await this.antennaNotesRepository.update({ antennaId: In(myAntennas.map(a => a.id)), @@ -191,14 +194,14 @@ export class NoteReadService { }, { read: true, }); - + // TODO: まとめてクエリしたい for (const antenna of myAntennas) { const count = await this.antennaNotesRepository.countBy({ antennaId: antenna.id, read: false, }); - + if (count === 0) { this.globalEventService.publishMainStream(userId, 'readAntenna', antenna); this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id }); @@ -213,4 +216,8 @@ export class NoteReadService { }); } } + + onApplicationShutdown(signal?: string | undefined): void { + this.#shutdownController.abort(); + } } diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 9fef36dd2c..88173c2307 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -2,13 +2,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { NotificationsRepository } from '@/models/index.js'; -import type { UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { Notification } from '@/models/entities/Notification.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { bindThis } from '@/decorators.js'; import { GlobalEventService } from './GlobalEventService.js'; import { PushNotificationService } from './PushNotificationService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class NotificationService { @@ -66,7 +65,6 @@ export class NotificationService { @bindThis private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { - this.globalEventService.publishMainStream(userId, 'readNotifications', notificationIds); return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds }); } } diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 042dcb3e67..368753d9a7 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -1,10 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; -import { Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository, User } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; import { RelayService } from '@/core/RelayService.js'; -import type { CacheableUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -39,7 +37,7 @@ export class PollService { } @bindThis - public async vote(user: CacheableUser, note: Note, choice: number) { + public async vote(user: User, note: Note, choice: number) { const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); if (poll == null) throw new Error('poll not found'); @@ -97,7 +95,7 @@ export class PollService { if (user == null) throw new Error('note not found'); if (this.userEntityService.isLocalUser(user)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); + const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); } diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts index 55b70bfc94..780e56ef10 100644 --- a/packages/backend/src/core/ProxyAccountService.ts +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository } from '@/models/index.js'; -import type { ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; @@ -16,9 +16,9 @@ export class ProxyAccountService { } @bindThis - public async fetch(): Promise { + public async fetch(): Promise { const meta = await this.metaService.fetch(); if (meta.proxyAccountId == null) return null; - return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as ILocalUser; + return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as LocalUser; } } diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index b18b7bb2cd..2cad1bc07e 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -9,24 +9,21 @@ import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; // Defined also packages/sw/types.ts#L13 -type pushNotificationsTypes = { +type PushNotificationsTypes = { 'notification': Packed<'Notification'>; - 'unreadMessagingMessage': Packed<'MessagingMessage'>; 'unreadAntennaNote': { antenna: { id: string, name: string }; note: Packed<'Note'>; }; 'readNotifications': { notificationIds: string[] }; 'readAllNotifications': undefined; - 'readAllMessagingMessages': undefined; - 'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string }; 'readAntenna': { antennaId: string }; 'readAllAntennas': undefined; }; // Reduce length because push message servers have character limits -function truncateBody(type: T, body: pushNotificationsTypes[T]): pushNotificationsTypes[T] { - if (body === undefined) return body; +function truncateBody(type: T, body: PushNotificationsTypes[T]): PushNotificationsTypes[T] { + if (typeof body !== 'object') return body; return { ...body, @@ -40,11 +37,9 @@ function truncateBody(type: T, body: pus reply: undefined, renote: undefined, user: type === 'notification' ? undefined as any : body.note.user, - } + }, } : {}), }; - - return body; } @Injectable() @@ -61,7 +56,7 @@ export class PushNotificationService { } @bindThis - public async pushNotification(userId: string, type: T, body: pushNotificationsTypes[T]) { + public async pushNotification(userId: string, type: T, body: PushNotificationsTypes[T]) { const meta = await this.metaService.fetch(); if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; @@ -81,8 +76,6 @@ export class PushNotificationService { if ([ 'readNotifications', 'readAllNotifications', - 'readAllMessagingMessages', - 'readAllMessagingMessagesOfARoom', 'readAntenna', 'readAllAntennas', ].includes(type) && !subscription.sendReadMessage) continue; diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 3806590059..9fccc14ee4 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -3,7 +3,7 @@ import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { IRemoteUser, User } from '@/models/entities/User.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { IdService } from '@/core/IdService.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js'; @@ -85,7 +85,7 @@ export class ReactionService { } @bindThis - public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) { + public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string | null) { // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -177,11 +177,11 @@ export class ReactionService { //#region 配信 if (this.userEntityService.isLocalUser(user) && !note.localOnly) { - const content = this.apRendererService.renderActivity(await this.apRendererService.renderLike(record, note)); + const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note)); const dm = this.apDeliverManagerService.createDeliverManager(user, content); if (note.userHost !== null) { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as IRemoteUser); + dm.addDirectRecipe(reactee as RemoteUser); } if (['public', 'home', 'followers'].includes(note.visibility)) { @@ -189,7 +189,7 @@ export class ReactionService { } else if (note.visibility === 'specified') { const visibleUsers = await Promise.all(note.visibleUserIds.map(id => this.usersRepository.findOneBy({ id }))); for (const u of visibleUsers.filter(u => u && this.userEntityService.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); + dm.addDirectRecipe(u as RemoteUser); } } @@ -235,11 +235,11 @@ export class ReactionService { //#region 配信 if (this.userEntityService.isLocalUser(user) && !note.localOnly) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user)); const dm = this.apDeliverManagerService.createDeliverManager(user, content); if (note.userHost !== null) { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as IRemoteUser); + dm.addDirectRecipe(reactee as RemoteUser); } dm.addFollowersRecipe(); dm.execute(); diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index a7408649b8..2e07825e9b 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; -import type { ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Cache } from '@/misc/cache.js'; @@ -34,16 +34,16 @@ export class RelayService { } @bindThis - private async getRelayActor(): Promise { + private async getRelayActor(): Promise { const user = await this.usersRepository.findOneBy({ host: IsNull(), username: ACTOR_USERNAME, }); - if (user) return user as ILocalUser; + if (user) return user as LocalUser; const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME); - return created as ILocalUser; + return created as LocalUser; } @bindThis @@ -56,7 +56,7 @@ export class RelayService { const relayActor = await this.getRelayActor(); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); - const activity = this.apRendererService.renderActivity(follow); + const activity = this.apRendererService.addContext(follow); this.queueService.deliver(relayActor, activity, relay.inbox); return relay; @@ -75,7 +75,7 @@ export class RelayService { const relayActor = await this.getRelayActor(); const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor); - const activity = this.apRendererService.renderActivity(undo); + const activity = this.apRendererService.addContext(undo); this.queueService.deliver(relayActor, activity, relay.inbox); await this.relaysRepository.delete(relay.id); diff --git a/packages/backend/src/core/RemoteLoggerService.ts b/packages/backend/src/core/RemoteLoggerService.ts index 0ea5d7b42f..3d45605836 100644 --- a/packages/backend/src/core/RemoteLoggerService.ts +++ b/packages/backend/src/core/RemoteLoggerService.ts @@ -1,7 +1,6 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class RemoteLoggerService { diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index dde4098624..b72dce5180 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -4,7 +4,7 @@ import chalk from 'chalk'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/index.js'; -import type { IRemoteUser, User } from '@/models/entities/User.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -60,7 +60,7 @@ export class RemoteUserResolveService { }); } - const user = await this.usersRepository.findOneBy({ usernameLower, host }) as IRemoteUser | null; + const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null; const acctLower = `${usernameLower}@${host}`; @@ -82,7 +82,7 @@ export class RemoteUserResolveService { const self = await this.resolveSelf(acctLower); if (user.uri !== self.href) { - // if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping. + // if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping. this.logger.info(`uri missmatch: ${acctLower}`); this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index d15d8c0aee..7149591198 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -3,7 +3,7 @@ import Redis from 'ioredis'; import { In } from 'typeorm'; import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; -import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js'; +import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; @@ -11,6 +11,8 @@ import { UserCacheService } from '@/core/UserCacheService.js'; import type { RoleCondFormulaValue } from '@/models/entities/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { StreamMessages } from '@/server/api/stream/types.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; export type RolePolicies = { @@ -56,6 +58,9 @@ export class RoleService implements OnApplicationShutdown { private rolesCache: Cache; private roleAssignmentByUserIdCache: Cache; + public static AlreadyAssignedError = class extends Error {}; + public static NotAssignedError = class extends Error {}; + constructor( @Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis, @@ -72,6 +77,8 @@ export class RoleService implements OnApplicationShutdown { private metaService: MetaService, private userCacheService: UserCacheService, private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private idService: IdService, ) { //this.onMessage = this.onMessage.bind(this); @@ -128,6 +135,7 @@ export class RoleService implements OnApplicationShutdown { cached.push({ ...body, createdAt: new Date(body.createdAt), + expiresAt: body.expiresAt ? new Date(body.expiresAt) : null, }); } break; @@ -193,7 +201,10 @@ export class RoleService implements OnApplicationShutdown { @bindThis public async getUserRoles(userId: User['id']) { - const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); + const now = Date.now(); + let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); + // 期限切れのロールを除外 + assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); const assignedRoleIds = assigns.map(x => x.roleId); const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); @@ -207,12 +218,21 @@ export class RoleService implements OnApplicationShutdown { */ @bindThis public async getUserBadgeRoles(userId: User['id']) { - const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); + const now = Date.now(); + let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); + // 期限切れのロールを除外 + assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); const assignedRoleIds = assigns.map(x => x.roleId); const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); - // コンディショナルロールも含めるのは負荷高そうだから一旦無し - return assignedBadgeRoles; + const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional')); + if (badgeCondRoles.length > 0) { + const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null; + const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula)); + return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; + } else { + return assignedBadgeRoles; + } } @bindThis @@ -310,6 +330,65 @@ export class RoleService implements OnApplicationShutdown { return users; } + @bindThis + public async assign(userId: User['id'], roleId: Role['id'], expiresAt: Date | null = null): Promise { + const now = new Date(); + + const existing = await this.roleAssignmentsRepository.findOneBy({ + roleId: roleId, + userId: userId, + }); + + if (existing) { + if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) { + await this.roleAssignmentsRepository.delete({ + roleId: roleId, + userId: userId, + }); + } else { + throw new RoleService.AlreadyAssignedError(); + } + } + + const created = await this.roleAssignmentsRepository.insert({ + id: this.idService.genId(), + createdAt: now, + expiresAt: expiresAt, + roleId: roleId, + userId: userId, + }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); + + this.rolesRepository.update(roleId, { + lastUsedAt: new Date(), + }); + + this.globalEventService.publishInternalEvent('userRoleAssigned', created); + } + + @bindThis + public async unassign(userId: User['id'], roleId: Role['id']): Promise { + const now = new Date(); + + const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId }); + if (existing == null) { + throw new RoleService.NotAssignedError(); + } else if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) { + await this.roleAssignmentsRepository.delete({ + roleId: roleId, + userId: userId, + }); + throw new RoleService.NotAssignedError(); + } + + await this.roleAssignmentsRepository.delete(existing.id); + + this.rolesRepository.update(roleId, { + lastUsedAt: now, + }); + + this.globalEventService.publishInternalEvent('userRoleUnassigned', existing); + } + @bindThis public onApplicationShutdown(signal?: string | undefined) { this.redisSubscriber.off('message', this.onMessage); diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index d734328669..be37bad52e 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import Redis from 'ioredis'; import { IdService } from '@/core/IdService.js'; -import type { CacheableUser, User } from '@/models/entities/User.js'; +import type { User } from '@/models/entities/User.js'; import type { Blocking } from '@/models/entities/Blocking.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -117,7 +117,7 @@ export class UserBlockingService implements OnApplicationShutdown { }); if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking)); + const content = this.apRendererService.addContext(this.apRendererService.renderBlock(blocking)); this.queueService.deliver(blocker, content, blockee.inbox); } } @@ -162,13 +162,13 @@ export class UserBlockingService implements OnApplicationShutdown { // リモートにフォローリクエストをしていたらUndoFollow送信 if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); this.queueService.deliver(follower, content, followee.inbox); } // リモートからフォローリクエストを受けていたらReject送信 if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); this.queueService.deliver(followee, content, follower.inbox); } } @@ -210,13 +210,13 @@ export class UserBlockingService implements OnApplicationShutdown { // リモートにフォローをしていたらUndoFollow送信 if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); this.queueService.deliver(follower, content, followee.inbox); } // リモートからフォローをされていたらRejectFollow送信 if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); this.queueService.deliver(followee, content, follower.inbox); } } @@ -236,7 +236,7 @@ export class UserBlockingService implements OnApplicationShutdown { } @bindThis - public async unblock(blocker: CacheableUser, blockee: CacheableUser) { + public async unblock(blocker: User, blockee: User) { const blocking = await this.blockingsRepository.findOneBy({ blockerId: blocker.id, blockeeId: blockee.id, @@ -261,7 +261,7 @@ export class UserBlockingService implements OnApplicationShutdown { // deliver if remote bloking if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); this.queueService.deliver(blocker, content, blockee.inbox); } } diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index 29a64f5848..fc383d1c08 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import type { UsersRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; -import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class UserCacheService implements OnApplicationShutdown { - public userByIdCache: Cache; - public localUserByNativeTokenCache: Cache; - public localUserByIdCache: Cache; - public uriPersonCache: Cache; + public userByIdCache: Cache; + public localUserByNativeTokenCache: Cache; + public localUserByIdCache: Cache; + public uriPersonCache: Cache; constructor( @Inject(DI.redisSubscriber) @@ -27,10 +27,10 @@ export class UserCacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new Cache(Infinity); - this.localUserByNativeTokenCache = new Cache(Infinity); - this.localUserByIdCache = new Cache(Infinity); - this.uriPersonCache = new Cache(Infinity); + this.userByIdCache = new Cache(Infinity); + this.localUserByNativeTokenCache = new Cache(Infinity); + this.localUserByIdCache = new Cache(Infinity); + this.uriPersonCache = new Cache(Infinity); this.redisSubscriber.on('message', this.onMessage); } @@ -58,7 +58,7 @@ export class UserCacheService implements OnApplicationShutdown { break; } case 'userTokenRegenerated': { - const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as ILocalUser; + const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser; this.localUserByNativeTokenCache.delete(body.oldToken); this.localUserByNativeTokenCache.set(body.newToken, user); break; diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 2214a4862a..d8426512bf 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { CacheableUser, ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueService } from '@/core/QueueService.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; @@ -21,16 +21,16 @@ import Logger from '../logger.js'; const logger = new Logger('following/create'); -type Local = ILocalUser | { - id: ILocalUser['id']; - host: ILocalUser['host']; - uri: ILocalUser['uri'] +type Local = LocalUser | { + id: LocalUser['id']; + host: LocalUser['host']; + uri: LocalUser['uri'] }; -type Remote = IRemoteUser | { - id: IRemoteUser['id']; - host: IRemoteUser['host']; - uri: IRemoteUser['uri']; - inbox: IRemoteUser['inbox']; +type Remote = RemoteUser | { + id: RemoteUser['id']; + host: RemoteUser['host']; + uri: RemoteUser['uri']; + inbox: RemoteUser['inbox']; }; type Both = Local | Remote; @@ -81,7 +81,7 @@ export class UserFollowingService { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); this.queueService.deliver(followee, content, follower.inbox); return; } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { @@ -130,7 +130,7 @@ export class UserFollowingService { await this.insertFollowingDoc(followee, follower); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); this.queueService.deliver(followee, content, follower.inbox); } } @@ -293,13 +293,13 @@ export class UserFollowingService { } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); this.queueService.deliver(follower, content, followee.inbox); } if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { // local user has null host - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); this.queueService.deliver(followee, content, follower.inbox); } } @@ -388,7 +388,7 @@ export class UserFollowingService { } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)); this.queueService.deliver(follower, content, followee.inbox); } } @@ -403,7 +403,7 @@ export class UserFollowingService { }, ): Promise { if (this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので this.queueService.deliver(follower, content, followee.inbox); @@ -434,7 +434,7 @@ export class UserFollowingService { followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, - follower: CacheableUser, + follower: User, ): Promise { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, @@ -448,7 +448,7 @@ export class UserFollowingService { await this.insertFollowingDoc(followee, follower); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); this.queueService.deliver(followee, content, follower.inbox); } @@ -556,7 +556,7 @@ export class UserFollowingService { followerId: follower.id, }); - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee)); this.queueService.deliver(followee, content, follower.inbox); } diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index c174394999..bc726a1feb 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -14,6 +14,8 @@ import { RoleService } from '@/core/RoleService.js'; @Injectable() export class UserListService { + public static TooManyUsersError = class extends Error {}; + constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -36,7 +38,7 @@ export class UserListService { userListId: list.id, }); if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { - throw new Error('Too many users'); + throw new UserListService.TooManyUsersError(); } await this.userListJoiningsRepository.insert({ diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index df1664942f..02903a0590 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -35,7 +35,7 @@ export class UserSuspendService { if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにDelete配信 - const content = this.apRendererService.renderActivity(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user)); + const content = this.apRendererService.addContext(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user)); const queue: string[] = []; @@ -65,7 +65,7 @@ export class UserSuspendService { if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにUndo Delete配信 - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user)); const queue: string[] = []; diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index ea5701decc..eccfeb0e7d 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -6,6 +6,7 @@ import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; import { createTempDir } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; +import { appendQuery, query } from '@/misc/prelude/url.js'; @Injectable() export class VideoProcessingService { @@ -41,5 +42,18 @@ export class VideoProcessingService { cleanup(); } } + + @bindThis + public getExternalVideoThumbnailUrl(url: string): string | null { + if (this.config.videoThumbnailGenerator == null) return null; + + return appendQuery( + `${this.config.videoThumbnailGenerator}/thumbnail.webp`, + query({ + thumbnail: '1', + url, + }) + ); + } } diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts index 30caa9682c..ac1e413de6 100644 --- a/packages/backend/src/core/WebhookService.ts +++ b/packages/backend/src/core/WebhookService.ts @@ -47,6 +47,7 @@ export class WebhookService implements OnApplicationShutdown { this.webhooks.push({ ...body, createdAt: new Date(body.createdAt), + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, }); } break; @@ -57,11 +58,13 @@ export class WebhookService implements OnApplicationShutdown { this.webhooks[i] = { ...body, createdAt: new Date(body.createdAt), + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, }; } else { this.webhooks.push({ ...body, createdAt: new Date(body.createdAt), + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, }); } } else { diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 64f01644a7..8282a6324c 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -1,11 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; +import { Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; -import { DI } from '@/di-symbols.js'; -import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js'; -import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; +import { concat, unique } from '@/misc/prelude/array.js'; import { bindThis } from '@/decorators.js'; -import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { getApIds } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { ApObject } from './type.js'; import type { Resolver } from './ApResolverService.js'; @@ -14,8 +12,8 @@ type Visibility = 'public' | 'home' | 'followers' | 'specified'; type AudienceInfo = { visibility: Visibility, - mentionedUsers: CacheableUser[], - visibleUsers: CacheableUser[], + mentionedUsers: User[], + visibleUsers: User[], }; @Injectable() @@ -26,16 +24,16 @@ export class ApAudienceService { } @bindThis - public async parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { + public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { const toGroups = this.groupingAudience(getApIds(to), actor); const ccGroups = this.groupingAudience(getApIds(cc), actor); const others = unique(concat([toGroups.other, ccGroups.other])); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), - )).filter((x): x is CacheableUser => x != null); + )).filter((x): x is User => x != null); if (toGroups.public.length > 0) { return { @@ -69,7 +67,7 @@ export class ApAudienceService { } @bindThis - private groupingAudience(ids: string[], actor: CacheableRemoteUser) { + private groupingAudience(ids: string[], actor: RemoteUser) { const groups = { public: [] as string[], followers: [] as string[], @@ -101,7 +99,7 @@ export class ApAudienceService { } @bindThis - private isFollowers(id: string, actor: CacheableRemoteUser) { + private isFollowers(id: string, actor: RemoteUser) { return ( id === (actor.followersUri ?? `${actor.uri}/followers`) ); diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 1d0c2d5da4..d0a4ad7a75 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -1,15 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; -import type { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; +import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js'; import { Cache } from '@/misc/cache.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import type { Note } from '@/models/entities/Note.js'; -import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; import { bindThis } from '@/decorators.js'; +import { RemoteUser, User } from '@/models/entities/User.js'; import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { IObject } from './type.js'; @@ -42,9 +41,6 @@ export class ApDbResolverService { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -101,28 +97,11 @@ export class ApDbResolverService { } } - @bindThis - public async getMessageFromApId(value: string | IObject): Promise { - const parsed = this.parseUri(value); - - if (parsed.local) { - if (parsed.type !== 'notes') return null; - - return await this.messagingMessagesRepository.findOneBy({ - id: parsed.id, - }); - } else { - return await this.messagingMessagesRepository.findOneBy({ - uri: parsed.uri, - }); - } - } - /** * AP Person => Misskey User in DB */ @bindThis - public async getUserFromApId(value: string | IObject): Promise { + public async getUserFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); if (parsed.local) { @@ -143,7 +122,7 @@ export class ApDbResolverService { */ @bindThis public async getAuthUserFromKeyId(keyId: string): Promise<{ - user: CacheableRemoteUser; + user: RemoteUser; key: UserPublickey; } | null> { const key = await this.publicKeyCache.fetch(keyId, async () => { @@ -159,7 +138,7 @@ export class ApDbResolverService { if (key == null) return null; return { - user: await this.userCacheService.findById(key.userId) as CacheableRemoteUser, + user: await this.userCacheService.findById(key.userId) as RemoteUser, key, }; } @@ -169,10 +148,10 @@ export class ApDbResolverService { */ @bindThis public async getAuthUserFromApId(uri: string): Promise<{ - user: CacheableRemoteUser; + user: RemoteUser; key: UserPublickey | null; } | null> { - const user = await this.apPersonService.resolvePerson(uri) as CacheableRemoteUser; + const user = await this.apPersonService.resolvePerson(uri) as RemoteUser; if (user == null) return null; diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 256cf12651..5e6ea69846 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -3,7 +3,7 @@ import { IsNull, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { QueueService } from '@/core/QueueService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -18,7 +18,7 @@ interface IFollowersRecipe extends IRecipe { interface IDirectRecipe extends IRecipe { type: 'Direct'; - to: IRemoteUser; + to: RemoteUser; } const isFollowers = (recipe: any): recipe is IFollowersRecipe => @@ -50,7 +50,7 @@ export class ApDeliverManagerService { * @param from Followee */ @bindThis - public async deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) { + public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -68,7 +68,7 @@ export class ApDeliverManagerService { * @param to Target user */ @bindThis - public async deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) { + public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -132,7 +132,7 @@ class DeliverManager { * @param to To */ @bindThis - public addDirectRecipe(to: IRemoteUser) { + public addDirectRecipe(to: RemoteUser) { const recipe = { type: 'Direct', to, diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 76c8bf68df..6d9569bce2 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { CacheableRemoteUser } from '@/models/entities/User.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ReactionService } from '@/core/ReactionService.js'; import { RelayService } from '@/core/RelayService.js'; @@ -20,9 +19,10 @@ import { UtilityService } from '@/core/UtilityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; -import { MessagingService } from '@/core/MessagingService.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingMessagesRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js'; -import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; +import type { RemoteUser } from '@/models/entities/User.js'; +import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -31,8 +31,7 @@ import { ApAudienceService } from './ApAudienceService.js'; import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; -import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IRead, IReject, IRemove, IUndo, IUpdate } from './type.js'; -import { bindThis } from '@/decorators.js'; +import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate } from './type.js'; @Injectable() export class ApInboxService { @@ -51,9 +50,6 @@ export class ApInboxService { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, @@ -81,13 +77,12 @@ export class ApInboxService { private apPersonService: ApPersonService, private apQuestionService: ApQuestionService, private queueService: QueueService, - private messagingService: MessagingService, ) { this.logger = this.apLoggerService.logger; } @bindThis - public async performActivity(actor: CacheableRemoteUser, activity: IObject) { + public async performActivity(actor: RemoteUser, activity: IObject) { if (isCollectionOrOrderedCollection(activity)) { const resolver = this.apResolverService.createResolver(); for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { @@ -115,7 +110,7 @@ export class ApInboxService { } @bindThis - public async performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise { + public async performOneActivity(actor: RemoteUser, activity: IObject): Promise { if (actor.isSuspended) return; if (isCreate(activity)) { @@ -124,8 +119,6 @@ export class ApInboxService { await this.delete(actor, activity); } else if (isUpdate(activity)) { await this.update(actor, activity); - } else if (isRead(activity)) { - await this.read(actor, activity); } else if (isFollow(activity)) { await this.follow(actor, activity); } else if (isAccept(activity)) { @@ -152,7 +145,7 @@ export class ApInboxService { } @bindThis - private async follow(actor: CacheableRemoteUser, activity: IFollow): Promise { + private async follow(actor: RemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); if (followee == null) { @@ -168,7 +161,7 @@ export class ApInboxService { } @bindThis - private async like(actor: CacheableRemoteUser, activity: ILike): Promise { + private async like(actor: RemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); @@ -186,30 +179,7 @@ export class ApInboxService { } @bindThis - private async read(actor: CacheableRemoteUser, activity: IRead): Promise { - const id = await getApId(activity.object); - - if (!this.utilityService.isSelfHost(this.utilityService.extractDbHost(id))) { - return `skip: Read to foreign host (${id})`; - } - - const messageId = id.split('/').pop(); - - const message = await this.messagingMessagesRepository.findOneBy({ id: messageId }); - if (message == null) { - return 'skip: message not found'; - } - - if (actor.id !== message.recipientId) { - return 'skip: actor is not a message recipient'; - } - - await this.messagingService.readUserMessagingMessage(message.recipientId!, message.userId, [message.id]); - return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`; - } - - @bindThis - private async accept(actor: CacheableRemoteUser, activity: IAccept): Promise { + private async accept(actor: RemoteUser, activity: IAccept): Promise { const uri = activity.id ?? activity; this.logger.info(`Accept: ${uri}`); @@ -227,7 +197,7 @@ export class ApInboxService { } @bindThis - private async acceptFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { + private async acceptFollow(actor: RemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const follower = await this.apDbResolverService.getUserFromApId(activity.actor); @@ -251,7 +221,7 @@ export class ApInboxService { } @bindThis - private async add(actor: CacheableRemoteUser, activity: IAdd): Promise { + private async add(actor: RemoteUser, activity: IAdd): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -271,7 +241,7 @@ export class ApInboxService { } @bindThis - private async announce(actor: CacheableRemoteUser, activity: IAnnounce): Promise { + private async announce(actor: RemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); this.logger.info(`Announce: ${uri}`); @@ -282,7 +252,7 @@ export class ApInboxService { } @bindThis - private async announceNote(actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise { + private async announceNote(actor: RemoteUser, activity: IAnnounce, targetUri: string): Promise { const uri = getApId(activity); if (actor.isSuspended) { @@ -342,7 +312,7 @@ export class ApInboxService { } @bindThis - private async block(actor: CacheableRemoteUser, activity: IBlock): Promise { + private async block(actor: RemoteUser, activity: IBlock): Promise { // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず const blockee = await this.apDbResolverService.getUserFromApId(activity.object); @@ -360,7 +330,7 @@ export class ApInboxService { } @bindThis - private async create(actor: CacheableRemoteUser, activity: ICreate): Promise { + private async create(actor: RemoteUser, activity: ICreate): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); @@ -396,7 +366,7 @@ export class ApInboxService { } @bindThis - private async createNote(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { + private async createNote(resolver: Resolver, actor: RemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { const uri = getApId(note); if (typeof note === 'object') { @@ -431,7 +401,7 @@ export class ApInboxService { } @bindThis - private async delete(actor: CacheableRemoteUser, activity: IDelete): Promise { + private async delete(actor: RemoteUser, activity: IDelete): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -473,16 +443,18 @@ export class ApInboxService { } @bindThis - private async deleteActor(actor: CacheableRemoteUser, uri: string): Promise { + private async deleteActor(actor: RemoteUser, uri: string): Promise { this.logger.info(`Deleting the Actor: ${uri}`); if (actor.uri !== uri) { return `skip: delete actor ${actor.uri} !== ${uri}`; } - const user = await this.usersRepository.findOneByOrFail({ id: actor.id }); - if (user.isDeleted) { - this.logger.info('skip: already deleted'); + const user = await this.usersRepository.findOneBy({ id: actor.id }); + if (user == null) { + return 'skip: actor not found'; + } else if (user.isDeleted) { + return 'skip: already deleted'; } const job = await this.queueService.createDeleteAccountJob(actor); @@ -495,7 +467,7 @@ export class ApInboxService { } @bindThis - private async deleteNote(actor: CacheableRemoteUser, uri: string): Promise { + private async deleteNote(actor: RemoteUser, uri: string): Promise { this.logger.info(`Deleting the Note: ${uri}`); const unlock = await this.appLockService.getApLock(uri); @@ -504,16 +476,7 @@ export class ApInboxService { const note = await this.apDbResolverService.getNoteFromApId(uri); if (note == null) { - const message = await this.apDbResolverService.getMessageFromApId(uri); - if (message == null) return 'message not found'; - - if (message.userId !== actor.id) { - return '投稿を削除しようとしているユーザーは投稿の作成者ではありません'; - } - - await this.messagingService.deleteMessage(message); - - return 'ok: message deleted'; + return 'message not found'; } if (note.userId !== actor.id) { @@ -528,7 +491,7 @@ export class ApInboxService { } @bindThis - private async flag(actor: CacheableRemoteUser, activity: IFlag): Promise { + private async flag(actor: RemoteUser, activity: IFlag): Promise { // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する const uris = getApIds(activity.object); @@ -553,7 +516,7 @@ export class ApInboxService { } @bindThis - private async reject(actor: CacheableRemoteUser, activity: IReject): Promise { + private async reject(actor: RemoteUser, activity: IReject): Promise { const uri = activity.id ?? activity; this.logger.info(`Reject: ${uri}`); @@ -571,7 +534,7 @@ export class ApInboxService { } @bindThis - private async rejectFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { + private async rejectFollow(actor: RemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const follower = await this.apDbResolverService.getUserFromApId(activity.actor); @@ -595,7 +558,7 @@ export class ApInboxService { } @bindThis - private async remove(actor: CacheableRemoteUser, activity: IRemove): Promise { + private async remove(actor: RemoteUser, activity: IRemove): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -615,7 +578,7 @@ export class ApInboxService { } @bindThis - private async undo(actor: CacheableRemoteUser, activity: IUndo): Promise { + private async undo(actor: RemoteUser, activity: IUndo): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -641,7 +604,7 @@ export class ApInboxService { } @bindThis - private async undoAccept(actor: CacheableRemoteUser, activity: IAccept): Promise { + private async undoAccept(actor: RemoteUser, activity: IAccept): Promise { const follower = await this.apDbResolverService.getUserFromApId(activity.object); if (follower == null) { return 'skip: follower not found'; @@ -661,7 +624,7 @@ export class ApInboxService { } @bindThis - private async undoAnnounce(actor: CacheableRemoteUser, activity: IAnnounce): Promise { + private async undoAnnounce(actor: RemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); const note = await this.notesRepository.findOneBy({ @@ -676,7 +639,7 @@ export class ApInboxService { } @bindThis - private async undoBlock(actor: CacheableRemoteUser, activity: IBlock): Promise { + private async undoBlock(actor: RemoteUser, activity: IBlock): Promise { const blockee = await this.apDbResolverService.getUserFromApId(activity.object); if (blockee == null) { @@ -692,7 +655,7 @@ export class ApInboxService { } @bindThis - private async undoFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { + private async undoFollow(actor: RemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); if (followee == null) { return 'skip: followee not found'; @@ -726,7 +689,7 @@ export class ApInboxService { } @bindThis - private async undoLike(actor: CacheableRemoteUser, activity: ILike): Promise { + private async undoLike(actor: RemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); @@ -741,7 +704,7 @@ export class ApInboxService { } @bindThis - private async update(actor: CacheableRemoteUser, activity: IUpdate): Promise { + private async update(actor: RemoteUser, activity: IUpdate): Promise { if ('actor' in activity && actor.uri !== activity.actor) { return 'skip: invalid actor'; } diff --git a/packages/backend/src/core/activitypub/ApLoggerService.ts b/packages/backend/src/core/activitypub/ApLoggerService.ts index b9bf1e4054..eeffab1b6d 100644 --- a/packages/backend/src/core/activitypub/ApLoggerService.ts +++ b/packages/backend/src/core/activitypub/ApLoggerService.ts @@ -1,7 +1,6 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApLoggerService { diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 648f30229a..6a1f233bd8 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid'; import * as mfm from 'mfm-js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js'; import type { Blocking } from '@/models/entities/Blocking.js'; import type { Relay } from '@/models/entities/Relay.js'; @@ -13,7 +13,6 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import type { Poll } from '@/models/entities/Poll.js'; -import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; import type { PollVote } from '@/models/entities/PollVote.js'; import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; import { MfmService } from '@/core/MfmService.js'; @@ -24,7 +23,7 @@ import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFil import { bindThis } from '@/decorators.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; -import type { IActivity, IObject } from './type.js'; +import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; import type { IIdentifier } from './models/identifier.js'; @Injectable() @@ -61,7 +60,7 @@ export class ApRendererService { } @bindThis - public renderAccept(object: any, user: { id: User['id']; host: null }) { + public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept { return { type: 'Accept', actor: `${this.config.url}/users/${user.id}`, @@ -70,7 +69,7 @@ export class ApRendererService { } @bindThis - public renderAdd(user: ILocalUser, target: any, object: any) { + public renderAdd(user: LocalUser, target: any, object: any): IAdd { return { type: 'Add', actor: `${this.config.url}/users/${user.id}`, @@ -80,7 +79,7 @@ export class ApRendererService { } @bindThis - public renderAnnounce(object: any, note: Note) { + public renderAnnounce(object: any, note: Note): IAnnounce { const attributedTo = `${this.config.url}/users/${note.userId}`; let to: string[] = []; @@ -93,7 +92,7 @@ export class ApRendererService { to = [`${attributedTo}/followers`]; cc = ['https://www.w3.org/ns/activitystreams#Public']; } else { - return null; + throw new Error('renderAnnounce: cannot render non-public note'); } return { @@ -113,7 +112,7 @@ export class ApRendererService { * @param block The block to be rendered. The blockee relation must be loaded. */ @bindThis - public renderBlock(block: Blocking) { + public renderBlock(block: Blocking): IBlock { if (block.blockee?.uri == null) { throw new Error('renderBlock: missing blockee uri'); } @@ -127,14 +126,14 @@ export class ApRendererService { } @bindThis - public renderCreate(object: any, note: Note) { + public renderCreate(object: IObject, note: Note): ICreate { const activity = { id: `${this.config.url}/notes/${note.id}/activity`, actor: `${this.config.url}/users/${note.userId}`, type: 'Create', published: note.createdAt.toISOString(), object, - } as any; + } as ICreate; if (object.to) activity.to = object.to; if (object.cc) activity.cc = object.cc; @@ -143,7 +142,7 @@ export class ApRendererService { } @bindThis - public renderDelete(object: any, user: { id: User['id']; host: null }) { + public renderDelete(object: IObject | string, user: { id: User['id']; host: null }): IDelete { return { type: 'Delete', actor: `${this.config.url}/users/${user.id}`, @@ -153,7 +152,7 @@ export class ApRendererService { } @bindThis - public renderDocument(file: DriveFile) { + public renderDocument(file: DriveFile): IApDocument { return { type: 'Document', mediaType: file.type, @@ -163,12 +162,12 @@ export class ApRendererService { } @bindThis - public renderEmoji(emoji: Emoji) { + public renderEmoji(emoji: Emoji): IApEmoji { return { id: `${this.config.url}/emojis/${emoji.name}`, type: 'Emoji', name: `:${emoji.name}:`, - updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString, + updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString(), icon: { type: 'Image', mediaType: emoji.type ?? 'image/png', @@ -179,9 +178,8 @@ export class ApRendererService { } // to anonymise reporters, the reporting actor must be a system user - // object has to be a uri or array of uris @bindThis - public renderFlag(user: ILocalUser, object: [string], content: string) { + public renderFlag(user: LocalUser, object: IObject | string, content: string): IFlag { return { type: 'Flag', actor: `${this.config.url}/users/${user.id}`, @@ -191,15 +189,13 @@ export class ApRendererService { } @bindThis - public renderFollowRelay(relay: Relay, relayActor: ILocalUser) { - const follow = { + public renderFollowRelay(relay: Relay, relayActor: LocalUser): IFollow { + return { id: `${this.config.url}/activities/follow-relay/${relay.id}`, type: 'Follow', actor: `${this.config.url}/users/${relayActor.id}`, object: 'https://www.w3.org/ns/activitystreams#Public', }; - - return follow; } /** @@ -217,19 +213,17 @@ export class ApRendererService { follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string, - ) { - const follow = { + ): IFollow { + return { id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`, type: 'Follow', - actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri, - object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri, - } as any; - - return follow; + actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri!, + object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri!, + }; } @bindThis - public renderHashtag(tag: string) { + public renderHashtag(tag: string): IApHashtag { return { type: 'Hashtag', href: `${this.config.url}/tags/${encodeURIComponent(tag)}`, @@ -238,7 +232,7 @@ export class ApRendererService { } @bindThis - public renderImage(file: DriveFile) { + public renderImage(file: DriveFile): IApImage { return { type: 'Image', url: this.driveFileEntityService.getPublicUrl(file), @@ -248,7 +242,7 @@ export class ApRendererService { } @bindThis - public renderKey(user: ILocalUser, key: UserKeypair, postfix?: string) { + public renderKey(user: LocalUser, key: UserKeypair, postfix?: string): IKey { return { id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, type: 'Key', @@ -261,7 +255,7 @@ export class ApRendererService { } @bindThis - public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }) { + public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise { const reaction = noteReaction.reaction; const object = { @@ -271,10 +265,11 @@ export class ApRendererService { object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`, content: reaction, _misskey_reaction: reaction, - } as any; + } as ILike; if (reaction.startsWith(':')) { const name = reaction.replaceAll(':', ''); + // TODO: cache const emoji = await this.emojisRepository.findOneBy({ name, host: IsNull(), @@ -287,16 +282,16 @@ export class ApRendererService { } @bindThis - public renderMention(mention: User) { + public renderMention(mention: User): IApMention { return { type: 'Mention', - href: this.userEntityService.isRemoteUser(mention) ? mention.uri : `${this.config.url}/users/${(mention as ILocalUser).id}`, - name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`, + href: this.userEntityService.isRemoteUser(mention) ? mention.uri! : `${this.config.url}/users/${(mention as LocalUser).id}`, + name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`, }; } @bindThis - public async renderNote(note: Note, dive = true, isTalk = false): Promise { + public async renderNote(note: Note, dive = true): Promise { const getPromisedFiles = async (ids: string[]) => { if (!ids || ids.length === 0) return []; const items = await this.driveFilesRepository.findBy({ id: In(ids) }); @@ -409,12 +404,8 @@ export class ApRendererService { totalItems: poll!.votes[i], }, })), - } : {}; - - const asTalk = isTalk ? { - _misskey_talk: true, - } : {}; - + } as const : {}; + return { id: `${this.config.url}/notes/${note.id}`, type: 'Note', @@ -436,12 +427,11 @@ export class ApRendererService { sensitive: note.cw != null || files.some(file => file.isSensitive), tag, ...asPoll, - ...asTalk, }; } @bindThis - public async renderPerson(user: ILocalUser) { + public async renderPerson(user: LocalUser) { const id = `${this.config.url}/users/${user.id}`; const isSystem = !!user.username.match(/\./); @@ -518,8 +508,8 @@ export class ApRendererService { } @bindThis - public async renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) { - const question = { + public renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll): IQuestion { + return { type: 'Question', id: `${this.config.url}/questions/${note.id}`, actor: `${this.config.url}/users/${user.id}`, @@ -533,21 +523,10 @@ export class ApRendererService { }, })), }; - - return question; } @bindThis - public renderRead(user: { id: User['id'] }, message: MessagingMessage) { - return { - type: 'Read', - actor: `${this.config.url}/users/${user.id}`, - object: message.uri, - }; - } - - @bindThis - public renderReject(object: any, user: { id: User['id'] }) { + public renderReject(object: any, user: { id: User['id'] }): IReject { return { type: 'Reject', actor: `${this.config.url}/users/${user.id}`, @@ -556,7 +535,7 @@ export class ApRendererService { } @bindThis - public renderRemove(user: { id: User['id'] }, target: any, object: any) { + public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove { return { type: 'Remove', actor: `${this.config.url}/users/${user.id}`, @@ -566,7 +545,7 @@ export class ApRendererService { } @bindThis - public renderTombstone(id: string) { + public renderTombstone(id: string): ITombstone { return { id, type: 'Tombstone', @@ -574,8 +553,7 @@ export class ApRendererService { } @bindThis - public renderUndo(object: any, user: { id: User['id'] }) { - if (object == null) return null; + public renderUndo(object: any, user: { id: User['id'] }): IUndo { const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; return { @@ -588,21 +566,19 @@ export class ApRendererService { } @bindThis - public renderUpdate(object: any, user: { id: User['id'] }) { - const activity = { + public renderUpdate(object: any, user: { id: User['id'] }): IUpdate { + return { id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, actor: `${this.config.url}/users/${user.id}`, type: 'Update', to: ['https://www.w3.org/ns/activitystreams#Public'], object, published: new Date().toISOString(), - } as any; - - return activity; + }; } @bindThis - public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser) { + public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: RemoteUser): ICreate { return { id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`, actor: `${this.config.url}/users/${user.id}`, @@ -621,9 +597,7 @@ export class ApRendererService { } @bindThis - public renderActivity(x: any): IActivity | null { - if (x == null) return null; - + public addContext(x: T): T & { '@context': any; id: string; } { if (typeof x === 'object' && x.id == null) { x.id = `${this.config.url}/${uuid()}`; } @@ -653,13 +627,12 @@ export class ApRendererService { '_misskey_quote': 'misskey:_misskey_quote', '_misskey_reaction': 'misskey:_misskey_reaction', '_misskey_votes': 'misskey:_misskey_votes', - '_misskey_talk': 'misskey:_misskey_talk', 'isCat': 'misskey:isCat', // vcard vcard: 'http://www.w3.org/2006/vcard/ns#', }, ], - }, x); + }, x as T & { id: string; }); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index bfd53dfabf..71fbc29476 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -28,6 +28,101 @@ type PrivateKey = { keyId: string; }; +export class ApRequestCreator { + static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record }): Signed { + const u = new URL(args.url); + const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; + + const request: Request = { + url: u.href, + method: 'POST', + headers: this.#objectAssignWithLcKey({ + 'Date': new Date().toUTCString(), + 'Host': u.host, + 'Content-Type': 'application/activity+json', + 'Digest': digestHeader, + }, args.additionalHeaders), + }; + + const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); + + return { + request, + signingString: result.signingString, + signature: result.signature, + signatureHeader: result.signatureHeader, + }; + } + + static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }): Signed { + const u = new URL(args.url); + + const request: Request = { + url: u.href, + method: 'GET', + headers: this.#objectAssignWithLcKey({ + 'Accept': 'application/activity+json, application/ld+json', + 'Date': new Date().toUTCString(), + 'Host': new URL(args.url).host, + }, args.additionalHeaders), + }; + + const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); + + return { + request, + signingString: result.signingString, + signature: result.signature, + signatureHeader: result.signatureHeader, + }; + } + + static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed { + const signingString = this.#genSigningString(request, includeHeaders); + const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64'); + const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`; + + request.headers = this.#objectAssignWithLcKey(request.headers, { + Signature: signatureHeader, + }); + // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! + delete request.headers['host']; + + return { + request, + signingString, + signature, + signatureHeader, + }; + } + + static #genSigningString(request: Request, includeHeaders: string[]): string { + request.headers = this.#lcObjectKey(request.headers); + + const results: string[] = []; + + for (const key of includeHeaders.map(x => x.toLowerCase())) { + if (key === '(request-target)') { + results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`); + } else { + results.push(`${key}: ${request.headers[key]}`); + } + } + + return results.join('\n'); + } + + static #lcObjectKey(src: Record): Record { + const dst: Record = {}; + for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; + return dst; + } + + static #objectAssignWithLcKey(a: Record, b: Record): Record { + return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b)); + } +} + @Injectable() export class ApRequestService { private logger: Logger; @@ -44,112 +139,13 @@ export class ApRequestService { this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる } - @bindThis - private createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record }): Signed { - const u = new URL(args.url); - const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`; - - const request: Request = { - url: u.href, - method: 'POST', - headers: this.objectAssignWithLcKey({ - 'Date': new Date().toUTCString(), - 'Host': u.host, - 'Content-Type': 'application/activity+json', - 'Digest': digestHeader, - }, args.additionalHeaders), - }; - - const result = this.signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); - - return { - request, - signingString: result.signingString, - signature: result.signature, - signatureHeader: result.signatureHeader, - }; - } - - @bindThis - private createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }): Signed { - const u = new URL(args.url); - - const request: Request = { - url: u.href, - method: 'GET', - headers: this.objectAssignWithLcKey({ - 'Accept': 'application/activity+json, application/ld+json', - 'Date': new Date().toUTCString(), - 'Host': new URL(args.url).host, - }, args.additionalHeaders), - }; - - const result = this.signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); - - return { - request, - signingString: result.signingString, - signature: result.signature, - signatureHeader: result.signatureHeader, - }; - } - - @bindThis - private signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed { - const signingString = this.genSigningString(request, includeHeaders); - const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64'); - const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`; - - request.headers = this.objectAssignWithLcKey(request.headers, { - Signature: signatureHeader, - }); - // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! - delete request.headers['host']; - - return { - request, - signingString, - signature, - signatureHeader, - }; - } - - @bindThis - private genSigningString(request: Request, includeHeaders: string[]): string { - request.headers = this.lcObjectKey(request.headers); - - const results: string[] = []; - - for (const key of includeHeaders.map(x => x.toLowerCase())) { - if (key === '(request-target)') { - results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`); - } else { - results.push(`${key}: ${request.headers[key]}`); - } - } - - return results.join('\n'); - } - - @bindThis - private lcObjectKey(src: Record): Record { - const dst: Record = {}; - for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; - return dst; - } - - @bindThis - private objectAssignWithLcKey(a: Record, b: Record): Record { - return Object.assign(this.lcObjectKey(a), this.lcObjectKey(b)); - } - @bindThis public async signedPost(user: { id: User['id'] }, url: string, object: any) { const body = JSON.stringify(object); const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); - const req = this.createSignedPost({ + const req = ApRequestCreator.createSignedPost({ key: { privateKeyPem: keypair.privateKey, keyId: `${this.config.url}/users/${user.id}#main-key`, @@ -176,7 +172,7 @@ export class ApRequestService { public async signedGet(url: string, user: { id: User['id'] }) { const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); - const req = this.createSignedGet({ + const req = ApRequestCreator.createSignedGet({ key: { privateKeyPem: keypair.privateKey, keyId: `${this.config.url}/users/${user.id}#main-key`, diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 7e962cb127..df7bb46405 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -18,7 +18,7 @@ import type { IObject, ICollection, IOrderedCollection } from './type.js'; export class Resolver { private history: Set; - private user?: ILocalUser; + private user?: LocalUser; private logger: Logger; constructor( @@ -38,8 +38,7 @@ export class Resolver { private recursionLimit = 100, ) { this.history = new Set(); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる + this.logger = this.loggerService.getLogger('ap-resolve'); } @bindThis @@ -124,17 +123,17 @@ export class Resolver { switch (parsed.type) { case 'notes': return this.notesRepository.findOneByOrFail({ id: parsed.id }) - .then(note => { + .then(async note => { if (parsed.rest === 'activity') { // this refers to the create activity and not the note itself - return this.apRendererService.renderActivity(this.apRendererService.renderCreate(this.apRendererService.renderNote(note), note)); + return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note), note)); } else { return this.apRendererService.renderNote(note); } }); case 'users': return this.usersRepository.findOneByOrFail({ id: parsed.id }) - .then(user => this.apRendererService.renderPerson(user as ILocalUser)); + .then(user => this.apRendererService.renderPerson(user as LocalUser)); case 'questions': // Polls are indexed by the note they are attached to. return Promise.all([ @@ -143,8 +142,8 @@ export class Resolver { ]) .then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll)); case 'likes': - return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(reaction => - this.apRendererService.renderActivity(this.apRendererService.renderLike(reaction, { uri: null }))!); + return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction => + this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null }))); case 'follows': // rest should be if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); @@ -152,7 +151,7 @@ export class Resolver { return Promise.all( [parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })), ) - .then(([follower, followee]) => this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee, url))); + .then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, url))); default: throw new Error(`resolveLocal: type ${parsed.type} unhandled`); } @@ -184,6 +183,7 @@ export class ApResolverService { private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, + private loggerService: LoggerService, ) { } @@ -202,6 +202,7 @@ export class ApResolverService { this.httpRequestService, this.apRendererService, this.apDbResolverService, + this.loggerService, ); } } diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index a29e1be564..2dc1a410ac 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -1,6 +1,5 @@ import * as crypto from 'node:crypto'; -import { Inject, Injectable } from '@nestjs/common'; -import jsonld from 'jsonld'; +import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { CONTEXTS } from './misc/contexts.js'; @@ -85,7 +84,9 @@ class LdSignature { @bindThis public async normalize(data: any) { const customLoader = this.getLoader(); - return await jsonld.normalize(data, { + // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically + // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 + return (await import('jsonld')).default.normalize(data, { documentLoader: customLoader, }); } diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 928ef1ae79..3b671af127 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { MetaService } from '@/core/MetaService.js'; import { truncate } from '@/misc/truncate.js'; @@ -36,7 +36,7 @@ export class ApImageService { * Imageを作成します。 */ @bindThis - public async createImage(actor: CacheableRemoteUser, value: any): Promise { + public async createImage(actor: RemoteUser, value: any): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); @@ -88,7 +88,7 @@ export class ApImageService { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveImage(actor: CacheableRemoteUser, value: any): Promise { + public async resolveImage(actor: RemoteUser, value: any): Promise { // TODO // リモートサーバーからフェッチしてきて登録 diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts index 41e6c6b14f..c581840ca9 100644 --- a/packages/backend/src/core/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts @@ -1,15 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { User } from '@/models/index.js'; import type { Config } from '@/config.js'; import { toArray, unique } from '@/misc/prelude/array.js'; -import type { CacheableUser } from '@/models/entities/User.js'; +import { bindThis } from '@/decorators.js'; import { isMention } from '../type.js'; import { ApResolverService, Resolver } from '../ApResolverService.js'; import { ApPersonService } from './ApPersonService.js'; import type { IObject, IApMention } from '../type.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApMentionService { @@ -26,10 +25,10 @@ export class ApMentionService { public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) { const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string)); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), - )).filter((x): x is CacheableUser => x != null); + )).filter((x): x is User => x != null); return mentionedUsers; } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 813415e6f6..c36e8d4ed6 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -1,9 +1,9 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; -import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js'; +import type { PollsRepository, EmojisRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; import type { Emoji } from '@/models/entities/Emoji.js'; @@ -16,7 +16,6 @@ import { IdService } from '@/core/IdService.js'; import { PollService } from '@/core/PollService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { MessagingService } from '@/core/MessagingService.js'; import { bindThis } from '@/decorators.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports @@ -47,9 +46,6 @@ export class ApNoteService { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - private idService: IdService, private apMfmService: ApMfmService, private apResolverService: ApResolverService, @@ -64,7 +60,6 @@ export class ApNoteService { private apImageService: ApImageService, private apQuestionService: ApQuestionService, private metaService: MetaService, - private messagingService: MessagingService, private appLockService: AppLockService, private pollService: PollService, private noteCreateService: NoteCreateService, @@ -114,7 +109,7 @@ export class ApNoteService { public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { if (resolver == null) resolver = this.apResolverService.createResolver(); - const object: any = await resolver.resolve(value); + const object = await resolver.resolve(value); const entryUri = getApId(value); const err = this.validateNote(object, entryUri); @@ -129,7 +124,7 @@ export class ApNoteService { throw new Error('invalid note'); } - const note: IPost = object; + const note: IPost = object as any; this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); @@ -146,7 +141,7 @@ export class ApNoteService { this.logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ - const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser; + const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { @@ -165,8 +160,6 @@ export class ApNoteService { } } - let isMessaging = note._misskey_talk && visibility === 'specified'; - const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); const apHashtags = await extractApHashtags(note.tag); @@ -193,17 +186,6 @@ export class ApNoteService { return x; } }).catch(async err => { - // トークだったらinReplyToのエラーは無視 - const uri = getApId(note.inReplyTo); - if (uri.startsWith(this.config.url + '/')) { - const id = uri.split('/').pop(); - const talk = await this.messagingMessagesRepository.findOneBy({ id }); - if (talk) { - isMessaging = true; - return null; - } - } - this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); throw err; }) @@ -292,14 +274,7 @@ export class ApNoteService { const apEmojis = emojis.map(emoji => emoji.name); const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); - - if (isMessaging) { - for (const recipient of visibleUsers) { - await this.messagingService.createMessage(actor, recipient, undefined, text ?? undefined, (files && files.length > 0) ? files[0] : null, object.id); - return null; - } - } - + return await this.noteCreateService.create(actor, { createdAt: note.published ? new Date(note.published) : null, files, diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 76f820cda0..a1fdd7a198 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -1,11 +1,11 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { CacheableUser, IRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js'; import { truncate } from '@/misc/truncate.js'; import type { UserCacheService } from '@/core/UserCacheService.js'; @@ -39,7 +39,7 @@ import type { ApResolverService, Resolver } from '../ApResolverService.js'; import type { ApLoggerService } from '../ApLoggerService.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { ApImageService } from './ApImageService.js'; -import type { IActor, IObject, IApPropertyValue } from '../type.js'; +import type { IActor, IObject } from '../type.js'; const nameLength = 128; const summaryLength = 2048; @@ -197,7 +197,7 @@ export class ApPersonService implements OnModuleInit { * Misskeyに対象のPersonが登録されていればそれを返します。 */ @bindThis - public async fetchPerson(uri: string, resolver?: Resolver): Promise { + public async fetchPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); const cached = this.userCacheService.uriPersonCache.get(uri); @@ -259,7 +259,7 @@ export class ApPersonService implements OnModuleInit { } // Create user - let user: IRemoteUser; + let user: RemoteUser; try { // Start transaction await this.db.transaction(async transactionalEntityManager => { @@ -284,7 +284,7 @@ export class ApPersonService implements OnModuleInit { isBot, isCat: (person as any).isCat === true, showTimelineReplies: false, - })) as IRemoteUser; + })) as RemoteUser; await transactionalEntityManager.save(new UserProfile({ userId: user.id, @@ -313,7 +313,7 @@ export class ApPersonService implements OnModuleInit { }); if (u) { - user = u as IRemoteUser; + user = u as RemoteUser; } else { throw new Error('already registered'); } @@ -392,7 +392,7 @@ export class ApPersonService implements OnModuleInit { } //#region このサーバーに既に登録されているか - const exist = await this.usersRepository.findOneBy({ uri }) as IRemoteUser; + const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser; if (exist == null) { return; @@ -500,7 +500,7 @@ export class ApPersonService implements OnModuleInit { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolvePerson(uri: string, resolver?: Resolver): Promise { + public async resolvePerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); //#region このサーバーに既に登録されていたらそれを返す diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index dcc5110aa5..7f2ca9c05e 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -1,25 +1,25 @@ -export type obj = { [x: string]: any }; +export type Obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; export interface IObject { - '@context': string | string[] | obj | obj[]; + '@context'?: string | string[] | Obj | Obj[]; type: string | string[]; id?: string; + name?: string | null; summary?: string; published?: string; cc?: ApObject; to?: ApObject; - attributedTo: ApObject; + attributedTo?: ApObject; attachment?: any[]; inReplyTo?: any; replies?: ICollection; - content?: string; - name?: string; + content?: string | null; startTime?: Date; endTime?: Date; icon?: any; image?: any; - url?: ApObject; + url?: ApObject | string; href?: string; tag?: IObject | IObject[]; sensitive?: boolean; @@ -113,11 +113,11 @@ export interface IPost extends IObject { _misskey_quote?: string; _misskey_content?: string; quoteUrl?: string; - _misskey_talk?: boolean; } export interface IQuestion extends IObject { type: 'Note' | 'Question'; + actor: string; source?: { content: string; mediaType: string; @@ -200,6 +200,7 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue => export interface IApMention extends IObject { type: 'Mention'; href: string; + name: string; } export const isMention = (object: IObject): object is IApMention => @@ -217,12 +218,30 @@ export const isHashtag = (object: IObject): object is IApHashtag => export interface IApEmoji extends IObject { type: 'Emoji'; - updated: Date; + name: string; + updated: string; } export const isEmoji = (object: IObject): object is IApEmoji => getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; +export interface IKey extends IObject { + type: 'Key'; + owner: string; + publicKeyPem: string | Buffer; +} + +export interface IApDocument extends IObject { + type: 'Document'; + name: string | null; + mediaType: string; +} + +export interface IApImage extends IObject { + type: 'Image'; + name: string | null; +} + export interface ICreate extends IActivity { type: 'Create'; } diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts index d392c6d595..afd3bab5a2 100644 --- a/packages/backend/src/core/chart/ChartLoggerService.ts +++ b/packages/backend/src/core/chart/ChartLoggerService.ts @@ -1,7 +1,6 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ChartLoggerService { diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index 779a32ac5e..03e3612658 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; import FederationChart from './charts/federation.js'; @@ -62,8 +62,10 @@ export class ChartManagementService implements OnApplicationShutdown { async onApplicationShutdown(signal: string): Promise { clearInterval(this.saveIntervalId); - await Promise.all( - this.charts.map(chart => chart.save()), - ); + if (process.env.NODE_ENV !== 'test') { + await Promise.all( + this.charts.map(chart => chart.save()), + ); + } } } diff --git a/packages/backend/src/core/chart/charts/drive.ts b/packages/backend/src/core/chart/charts/drive.ts index da36b944f5..b63db591fb 100644 --- a/packages/backend/src/core/chart/charts/drive.ts +++ b/packages/backend/src/core/chart/charts/drive.ts @@ -1,5 +1,5 @@ import { Injectable, Inject } from '@nestjs/common'; -import { Not, IsNull, DataSource } from 'typeorm'; +import { DataSource } from 'typeorm'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts index 1e2a579dfa..d8966f34c1 100644 --- a/packages/backend/src/core/chart/charts/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/per-user-notes.ts @@ -45,8 +45,8 @@ export default class PerUserNotesChart extends Chart { } @bindThis - public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise { - await this.commit({ + public update(user: { id: User['id'] }, note: Note, isAdditional: boolean): void { + this.commit({ 'total': isAdditional ? 1 : -1, 'inc': isAdditional ? 1 : 0, 'dec': isAdditional ? 0 : 1, diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index bc79ce26aa..c8a98c0bea 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { AntennaNotesRepository, AntennasRepository } from '@/models/index.js'; import type { Packed } from '@/misc/schema.js'; import type { Antenna } from '@/models/entities/Antenna.js'; import { bindThis } from '@/decorators.js'; @@ -14,9 +13,6 @@ export class AntennaEntityService { @Inject(DI.antennaNotesRepository) private antennaNotesRepository: AntennaNotesRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, ) { } @@ -27,7 +23,6 @@ export class AntennaEntityService { const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src }); const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null; - const userGroupJoining = antenna.userGroupJoiningId ? await this.userGroupJoiningsRepository.findOneBy({ id: antenna.userGroupJoiningId }) : null; return { id: antenna.id, @@ -37,7 +32,6 @@ export class AntennaEntityService { excludeKeywords: antenna.excludeKeywords, src: antenna.src, userListId: antenna.userListId, - userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null, users: antenna.users, caseSensitive: antenna.caseSensitive, notify: antenna.notify, diff --git a/packages/backend/src/core/entities/AppEntityService.ts b/packages/backend/src/core/entities/AppEntityService.ts index 781cbdcc6b..36cd48f3ce 100644 --- a/packages/backend/src/core/entities/AppEntityService.ts +++ b/packages/backend/src/core/entities/AppEntityService.ts @@ -1,11 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, AppsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { App } from '@/models/entities/App.js'; import type { User } from '@/models/entities/User.js'; -import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; @Injectable() diff --git a/packages/backend/src/core/entities/AuthSessionEntityService.ts b/packages/backend/src/core/entities/AuthSessionEntityService.ts index 4a74f9c2f6..b7edc8494e 100644 --- a/packages/backend/src/core/entities/AuthSessionEntityService.ts +++ b/packages/backend/src/core/entities/AuthSessionEntityService.ts @@ -2,10 +2,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { AuthSessionsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; import type { AuthSession } from '@/models/entities/AuthSession.js'; import type { User } from '@/models/entities/User.js'; -import { UserEntityService } from './UserEntityService.js'; import { AppEntityService } from './AppEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 6ce590aa96..0a9bcf85c4 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts index 1e794391e9..63c50865e0 100644 --- a/packages/backend/src/core/entities/ClipEntityService.ts +++ b/packages/backend/src/core/entities/ClipEntityService.ts @@ -4,7 +4,6 @@ import type { ClipsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; import type { Clip } from '@/models/entities/Clip.js'; import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 9dd115d45a..158fafa9d5 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -1,6 +1,5 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; -import * as mfm from 'mfm-js'; +import { DataSource } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -11,6 +10,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import { appendQuery, query } from '@/misc/prelude/url.js'; import { deepClone } from '@/misc/clone.js'; import { UtilityService } from '../UtilityService.js'; +import { VideoProcessingService } from '../VideoProcessingService.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFolderEntityService } from './DriveFolderEntityService.js'; @@ -43,6 +43,7 @@ export class DriveFileEntityService { private utilityService: UtilityService, private driveFolderEntityService: DriveFolderEntityService, + private videoProcessingService: VideoProcessingService, ) { } @@ -72,40 +73,63 @@ export class DriveFileEntityService { } @bindThis - public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail - const proxiedUrl = (url: string) => appendQuery( + private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string { + return appendQuery( `${this.config.mediaProxy}/${mode ?? 'image'}.webp`, query({ url, ...(mode ? { [mode]: '1' } : {}), - }) + }), ); + } + @bindThis + public getThumbnailUrl(file: DriveFile): string | null { + if (file.type.startsWith('video')) { + if (file.thumbnailUrl) return file.thumbnailUrl; + + if (this.config.videoThumbnailGenerator == null) { + return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri); + } + } else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { + // 動画ではなくリモートかつメディアプロキシ + return this.getProxiedUrl(file.uri, 'static'); + } + + if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { + // リモートかつ期限切れはローカルプロキシを試みる + // 従来は/files/${thumbnailAccessKey}にアクセスしていたが、 + // /filesはメディアプロキシにリダイレクトするようにしたため直接メディアプロキシを指定する + return this.getProxiedUrl(file.uri, 'static'); + } + + const url = file.webpublicUrl ?? file.url; + + return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? this.getProxiedUrl(url, 'static') : null); + } + + @bindThis + public getPublicUrl(file: DriveFile, mode?: 'avatar'): string { // static = thumbnail // リモートかつメディアプロキシ if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { - if (!(mode === 'static' && file.type.startsWith('video'))) { - return proxiedUrl(file.uri); - } + return this.getProxiedUrl(file.uri, mode); } // リモートかつ期限切れはローカルプロキシを試みる if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { - const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey; + const key = file.webpublicAccessKey; if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 const url = `${this.config.url}/files/${key}`; - if (mode === 'avatar') return proxiedUrl(file.uri); + if (mode === 'avatar') return this.getProxiedUrl(file.uri, 'avatar'); return url; } } const url = file.webpublicUrl ?? file.url; - if (mode === 'static') { - return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null); - } if (mode === 'avatar') { - return proxiedUrl(url); + return this.getProxiedUrl(url, 'avatar'); } return url; } @@ -183,7 +207,7 @@ export class DriveFileEntityService { blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), - thumbnailUrl: this.getPublicUrl(file, 'static'), + thumbnailUrl: this.getThumbnailUrl(file), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { @@ -218,7 +242,7 @@ export class DriveFileEntityService { blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), - thumbnailUrl: this.getPublicUrl(file, 'static'), + thumbnailUrl: this.getThumbnailUrl(file), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts index 0bb0f1754e..93c52c91f2 100644 --- a/packages/backend/src/core/entities/DriveFolderEntityService.ts +++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts @@ -4,9 +4,7 @@ import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/inde import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; import type { DriveFolder } from '@/models/entities/DriveFolder.js'; -import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; @Injectable() diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 7d248e6342..f5c8f2d4bb 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -1,50 +1,63 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { EmojisRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import { bindThis } from '@/decorators.js'; -import { UserEntityService } from './UserEntityService.js'; @Injectable() export class EmojiEntityService { constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - - private userEntityService: UserEntityService, ) { } @bindThis - public async pack( + public async packSimple( src: Emoji['id'] | Emoji, - opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = { omitHost: true, omitId: true, withUrl: true }, - ): Promise> { - opts = { omitHost: true, omitId: true, withUrl: true, ...opts } - + ): Promise> { const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); return { - id: opts.omitId ? undefined : emoji.id, aliases: emoji.aliases, name: emoji.name, category: emoji.category, - host: opts.omitHost ? undefined : emoji.host, // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) - url: opts.withUrl ? (emoji.publicUrl || emoji.originalUrl) : undefined, + url: emoji.publicUrl || emoji.originalUrl, }; } @bindThis - public packMany( + public packSimpleMany( emojis: any[], - opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {}, ) { - return Promise.all(emojis.map(x => this.pack(x, opts))); + return Promise.all(emojis.map(x => this.packSimple(x))); + } + + @bindThis + public async packDetailed( + src: Emoji['id'] | Emoji, + ): Promise> { + const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); + + return { + id: emoji.id, + aliases: emoji.aliases, + name: emoji.name, + category: emoji.category, + host: emoji.host, + // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) + url: emoji.publicUrl || emoji.originalUrl, + }; + } + + @bindThis + public packDetailedMany( + emojis: any[], + ) { + return Promise.all(emojis.map(x => this.packDetailed(x))); } } diff --git a/packages/backend/src/core/entities/FlashLikeEntityService.ts b/packages/backend/src/core/entities/FlashLikeEntityService.ts index dcf12d53ea..0351ec3014 100644 --- a/packages/backend/src/core/entities/FlashLikeEntityService.ts +++ b/packages/backend/src/core/entities/FlashLikeEntityService.ts @@ -1,13 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { FlashLikesRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { FlashLike } from '@/models/entities/FlashLike.js'; import { bindThis } from '@/decorators.js'; -import { UserEntityService } from './UserEntityService.js'; import { FlashEntityService } from './FlashEntityService.js'; @Injectable() diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts index 88c91d0f21..c2edc6a13a 100644 --- a/packages/backend/src/core/entities/FollowRequestEntityService.ts +++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts @@ -1,8 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { FollowRequestsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { FollowRequest } from '@/models/entities/FollowRequest.js'; diff --git a/packages/backend/src/core/entities/GalleryLikeEntityService.ts b/packages/backend/src/core/entities/GalleryLikeEntityService.ts index 8b15ffc2bb..db46045db3 100644 --- a/packages/backend/src/core/entities/GalleryLikeEntityService.ts +++ b/packages/backend/src/core/entities/GalleryLikeEntityService.ts @@ -1,12 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { GalleryLikesRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; import type { GalleryLike } from '@/models/entities/GalleryLike.js'; -import { UserEntityService } from './UserEntityService.js'; import { GalleryPostEntityService } from './GalleryPostEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/HashtagEntityService.ts b/packages/backend/src/core/entities/HashtagEntityService.ts index f79b821222..88b1ff3a38 100644 --- a/packages/backend/src/core/entities/HashtagEntityService.ts +++ b/packages/backend/src/core/entities/HashtagEntityService.ts @@ -1,10 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { HashtagsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; import type { Hashtag } from '@/models/entities/Hashtag.js'; import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 42ea5e23f6..8a9706816b 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -1,10 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { InstancesRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; import type { Instance } from '@/models/entities/Instance.js'; import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '../UtilityService.js'; diff --git a/packages/backend/src/core/entities/MessagingMessageEntityService.ts b/packages/backend/src/core/entities/MessagingMessageEntityService.ts deleted file mode 100644 index cdb752dd81..0000000000 --- a/packages/backend/src/core/entities/MessagingMessageEntityService.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { MessagingMessagesRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; -import { UserEntityService } from './UserEntityService.js'; -import { DriveFileEntityService } from './DriveFileEntityService.js'; -import { UserGroupEntityService } from './UserGroupEntityService.js'; -import { bindThis } from '@/decorators.js'; - -@Injectable() -export class MessagingMessageEntityService { - constructor( - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - - private userEntityService: UserEntityService, - private userGroupEntityService: UserGroupEntityService, - private driveFileEntityService: DriveFileEntityService, - ) { - } - - @bindThis - public async pack( - src: MessagingMessage['id'] | MessagingMessage, - me?: { id: User['id'] } | null | undefined, - options?: { - populateRecipient?: boolean, - populateGroup?: boolean, - }, - ): Promise> { - const opts = options ?? { - populateRecipient: true, - populateGroup: true, - }; - - const message = typeof src === 'object' ? src : await this.messagingMessagesRepository.findOneByOrFail({ id: src }); - - return { - id: message.id, - createdAt: message.createdAt.toISOString(), - text: message.text, - userId: message.userId, - user: await this.userEntityService.pack(message.user ?? message.userId, me), - recipientId: message.recipientId, - recipient: message.recipientId && opts.populateRecipient ? await this.userEntityService.pack(message.recipient ?? message.recipientId, me) : undefined, - groupId: message.groupId, - group: message.groupId && opts.populateGroup ? await this.userGroupEntityService.pack(message.group ?? message.groupId) : undefined, - fileId: message.fileId, - file: message.fileId ? await this.driveFileEntityService.pack(message.fileId) : null, - isRead: message.isRead, - reads: message.reads, - }; - } -} - diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts index ab61797910..7058e38af9 100644 --- a/packages/backend/src/core/entities/ModerationLogEntityService.ts +++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts @@ -2,9 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { ModerationLogsRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; import type { ModerationLog } from '@/models/entities/ModerationLog.js'; import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index bd6971adb3..2ffe5f1c21 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -1,9 +1,8 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { DataSource, In } from 'typeorm'; import * as mfm from 'mfm-js'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; import type { Packed } from '@/misc/schema.js'; import { nyaize } from '@/misc/nyaize.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; diff --git a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts index aa5c354b6d..8a7727b4cd 100644 --- a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts +++ b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts @@ -1,12 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { NoteFavoritesRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { NoteFavorite } from '@/models/entities/NoteFavorite.js'; -import { UserEntityService } from './UserEntityService.js'; import { NoteEntityService } from './NoteEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts index eba6f9d908..9779489673 100644 --- a/packages/backend/src/core/entities/NoteReactionEntityService.ts +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { NoteReactionsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { OnModuleInit } from '@nestjs/common'; import type { } from '@/models/entities/Blocking.js'; diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 4140b3f35e..be88a213f4 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -1,25 +1,25 @@ import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository, User } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Notification } from '@/models/entities/Notification.js'; -import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import type { Note } from '@/models/entities/Note.js'; import type { Packed } from '@/misc/schema.js'; import { bindThis } from '@/decorators.js'; +import { isNotNull } from '@/misc/is-not-null.js'; +import { notificationTypes } from '@/types.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'; -import type { UserGroupInvitationEntityService } from './UserGroupInvitationEntityService.js'; + +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { private userEntityService: UserEntityService; private noteEntityService: NoteEntityService; - private userGroupInvitationEntityService: UserGroupInvitationEntityService; private customEmojiService: CustomEmojiService; constructor( @@ -36,7 +36,6 @@ export class NotificationEntityService implements OnModuleInit { //private userEntityService: UserEntityService, //private noteEntityService: NoteEntityService, - //private userGroupInvitationEntityService: UserGroupInvitationEntityService, //private customEmojiService: CustomEmojiService, ) { } @@ -44,7 +43,6 @@ export class NotificationEntityService implements OnModuleInit { onModuleInit() { this.userEntityService = this.moduleRef.get('UserEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); - this.userGroupInvitationEntityService = this.moduleRef.get('UserGroupInvitationEntityService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); } @@ -52,13 +50,20 @@ export class NotificationEntityService implements OnModuleInit { public async pack( src: Notification['id'] | Notification, options: { - _hintForEachNotes_?: { - myReactions: Map; + _hint_?: { + packedNotes: Map>; }; }, ): Promise> { const notification = typeof src === 'object' ? src : await this.notificationsRepository.findOneByOrFail({ id: src }); const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null; + const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? ( + options._hint_?.packedNotes != null + ? options._hint_.packedNotes.get(notification.noteId) + : this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { + detail: true, + }) + ) : undefined; return await awaitAll({ id: notification.id, @@ -67,53 +72,10 @@ export class NotificationEntityService implements OnModuleInit { isRead: notification.isRead, userId: notification.notifierId, user: notification.notifierId ? this.userEntityService.pack(notification.notifier ?? notification.notifierId) : null, - ...(notification.type === 'mention' ? { - note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'reply' ? { - note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'renote' ? { - note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'quote' ? { - note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), + ...(noteIfNeed != null ? { note: noteIfNeed } : {}), ...(notification.type === 'reaction' ? { - note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), reaction: notification.reaction, } : {}), - ...(notification.type === 'pollVote' ? { // TODO: そのうち消す - note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - choice: notification.choice, - } : {}), - ...(notification.type === 'pollEnded' ? { - note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'groupInvited' ? { - invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!), - } : {}), ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), @@ -125,32 +87,32 @@ export class NotificationEntityService implements OnModuleInit { }); } + /** + * @param notifications you should join "note" property when fetch from DB, and all notifieeId should be same as meId + */ @bindThis public async packMany( notifications: Notification[], meId: User['id'], ) { if (notifications.length === 0) return []; - - const notes = notifications.filter(x => x.note != null).map(x => x.note!); - const noteIds = notes.map(n => n.id); - const myReactionsMap = new Map(); - const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); - const targets = [...noteIds, ...renoteIds]; - const myReactions = await this.noteReactionsRepository.findBy({ - userId: meId, - noteId: In(targets), - }); - - for (const target of targets) { - myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null); + + for (const notification of notifications) { + if (meId !== notification.notifieeId) { + // because we call note packMany with meId, all notifieeId should be same as meId + throw new Error('TRY_TO_PACK_ANOTHER_USER_NOTIFICATION'); + } } - await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes)); + const notes = notifications.map(x => x.note).filter(isNotNull); + const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, { + detail: true, + }); + const packedNotes = new Map(packedNotesArray.map(p => [p.id, p])); return await Promise.all(notifications.map(x => this.pack(x, { - _hintForEachNotes_: { - myReactions: myReactionsMap, + _hint_: { + packedNotes, }, }))); } diff --git a/packages/backend/src/core/entities/PageLikeEntityService.ts b/packages/backend/src/core/entities/PageLikeEntityService.ts index d3e45783dd..3460c1e422 100644 --- a/packages/backend/src/core/entities/PageLikeEntityService.ts +++ b/packages/backend/src/core/entities/PageLikeEntityService.ts @@ -1,12 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { PageLikesRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; import type { User } from '@/models/entities/User.js'; import type { PageLike } from '@/models/entities/PageLike.js'; -import { UserEntityService } from './UserEntityService.js'; import { PageEntityService } from './PageEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index dbb89ff19b..2f1d51fa1a 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; import type { User } from '@/models/entities/User.js'; import type { Role } from '@/models/entities/Role.js'; import { bindThis } from '@/decorators.js'; @@ -26,19 +26,16 @@ export class RoleEntityService { public async pack( src: Role['id'] | Role, me?: { id: User['id'] } | null | undefined, - options?: { - detail?: boolean; - }, ) { - const opts = Object.assign({ - detail: true, - }, options); - const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); - const assigns = await this.roleAssignmentsRepository.findBy({ - roleId: role.id, - }); + const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign') + .where('assign.roleId = :roleId', { roleId: role.id }) + .andWhere(new Brackets(qb => { qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); + })) + .getCount(); const policies = { ...role.policies }; for (const [k, v] of Object.entries(DEFAULT_POLICIES)) { @@ -65,10 +62,7 @@ export class RoleEntityService { asBadge: role.asBadge, canEditMembersByModerator: role.canEditMembersByModerator, policies: policies, - usersCount: assigns.length, - ...(opts.detail ? { - users: this.userEntityService.packMany(assigns.map(x => x.userId), me), - } : {}), + usersCount: assignedCount, }); } @@ -76,11 +70,8 @@ export class RoleEntityService { public packMany( roles: any[], me: { id: User['id'] }, - options?: { - detail?: boolean; - }, ) { - return Promise.all(roles.map(x => this.pack(x, me, options))); + return Promise.all(roles.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/SigninEntityService.ts b/packages/backend/src/core/entities/SigninEntityService.ts index c402644742..51fa7543d9 100644 --- a/packages/backend/src/core/entities/SigninEntityService.ts +++ b/packages/backend/src/core/entities/SigninEntityService.ts @@ -1,10 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { SigninsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; import type { Signin } from '@/models/entities/Signin.js'; import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index eea9d5567d..8c36e47f1b 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -1,4 +1,4 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { In, Not } from 'typeorm'; import Ajv from 'ajv'; import { ModuleRef } from '@nestjs/core'; @@ -10,9 +10,9 @@ import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; import { Cache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; -import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; -import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js'; +import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -32,13 +32,13 @@ type IsMeAndIsUserDetailed(user: T): user is T & { host: null; }; function isLocalUser(user: User | { host: User['host'] }): boolean { return user.host == null; } -function isRemoteUser(user: User): user is IRemoteUser; +function isRemoteUser(user: User): user is RemoteUser; function isRemoteUser(user: T): user is T & { host: string; }; function isRemoteUser(user: User | { host: User['host'] }): boolean { return !isLocalUser(user); @@ -102,12 +102,6 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.announcementReadsRepository) private announcementReadsRepository: AnnouncementReadsRepository, - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, @@ -204,36 +198,6 @@ export class UserEntityService implements OnModuleInit { }); } - @bindThis - public async getHasUnreadMessagingMessage(userId: User['id']): Promise { - const mute = await this.mutingsRepository.findBy({ - muterId: userId, - }); - - const joinings = await this.userGroupJoiningsRepository.findBy({ userId: userId }); - - const groupQs = Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder('message') - .where('message.groupId = :groupId', { groupId: j.userGroupId }) - .andWhere('message.userId != :userId', { userId: userId }) - .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) - .andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない - .getOne().then(x => x != null))); - - const [withUser, withGroups] = await Promise.all([ - this.messagingMessagesRepository.count({ - where: { - recipientId: userId, - isRead: false, - ...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}), - }, - take: 1, - }).then(count => count > 0), - groupQs, - ]); - - return withUser || withGroups.some(x => x); - } - @bindThis public async getHasUnreadAnnouncement(userId: User['id']): Promise { const reads = await this.announcementReadsRepository.findBy({ @@ -492,7 +456,6 @@ export class UserEntityService implements OnModuleInit { hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id), hasUnreadAntenna: this.getHasUnreadAntenna(user.id), hasUnreadChannel: this.getHasUnreadChannel(user.id), - hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), hasUnreadNotification: this.getHasUnreadNotification(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), mutedWords: profile!.mutedWords, diff --git a/packages/backend/src/core/entities/UserGroupEntityService.ts b/packages/backend/src/core/entities/UserGroupEntityService.ts deleted file mode 100644 index 0674a76723..0000000000 --- a/packages/backend/src/core/entities/UserGroupEntityService.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { UserGroupJoiningsRepository, UserGroupsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { UserGroup } from '@/models/entities/UserGroup.js'; -import { UserEntityService } from './UserEntityService.js'; -import { bindThis } from '@/decorators.js'; - -@Injectable() -export class UserGroupEntityService { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private userEntityService: UserEntityService, - ) { - } - - @bindThis - public async pack( - src: UserGroup['id'] | UserGroup, - ): Promise> { - const userGroup = typeof src === 'object' ? src : await this.userGroupsRepository.findOneByOrFail({ id: src }); - - const users = await this.userGroupJoiningsRepository.findBy({ - userGroupId: userGroup.id, - }); - - return { - id: userGroup.id, - createdAt: userGroup.createdAt.toISOString(), - name: userGroup.name, - ownerId: userGroup.userId, - userIds: users.map(x => x.userId), - }; - } -} - diff --git a/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts b/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts deleted file mode 100644 index 0fba1426f4..0000000000 --- a/packages/backend/src/core/entities/UserGroupInvitationEntityService.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { UserGroupInvitationsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; -import { UserEntityService } from './UserEntityService.js'; -import { UserGroupEntityService } from './UserGroupEntityService.js'; -import { bindThis } from '@/decorators.js'; - -@Injectable() -export class UserGroupInvitationEntityService { - constructor( - @Inject(DI.userGroupInvitationsRepository) - private userGroupInvitationsRepository: UserGroupInvitationsRepository, - - private userGroupEntityService: UserGroupEntityService, - ) { - } - - @bindThis - public async pack( - src: UserGroupInvitation['id'] | UserGroupInvitation, - ) { - const invitation = typeof src === 'object' ? src : await this.userGroupInvitationsRepository.findOneByOrFail({ id: src }); - - return { - id: invitation.id, - group: await this.userGroupEntityService.pack(invitation.userGroup ?? invitation.userGroupId), - }; - } - - @bindThis - public packMany( - invitations: any[], - ) { - return Promise.all(invitations.map(x => this.pack(x))); - } -} - diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index f2e0426928..3c1087881a 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -1,10 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/schema.js'; import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; import type { UserList } from '@/models/entities/UserList.js'; import { UserEntityService } from './UserEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/daemons/QueueStatsService.ts b/packages/backend/src/daemons/QueueStatsService.ts index 7b47d78a17..b717434e09 100644 --- a/packages/backend/src/daemons/QueueStatsService.ts +++ b/packages/backend/src/daemons/QueueStatsService.ts @@ -1,6 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import Xev from 'xev'; -import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; import { bindThis } from '@/decorators.js'; import type { OnApplicationShutdown } from '@nestjs/common'; diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index 7971f9e810..1688bb2ba7 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -1,8 +1,7 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; -import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { OnApplicationShutdown } from '@nestjs/common'; diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 3fb0cd4dae..05603093be 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -24,9 +24,6 @@ export const DI = { userPublickeysRepository: Symbol('userPublickeysRepository'), userListsRepository: Symbol('userListsRepository'), userListJoiningsRepository: Symbol('userListJoiningsRepository'), - userGroupsRepository: Symbol('userGroupsRepository'), - userGroupJoiningsRepository: Symbol('userGroupJoiningsRepository'), - userGroupInvitationsRepository: Symbol('userGroupInvitationsRepository'), userNotePiningsRepository: Symbol('userNotePiningsRepository'), userIpsRepository: Symbol('userIpsRepository'), usedUsernamesRepository: Symbol('usedUsernamesRepository'), @@ -47,7 +44,6 @@ export const DI = { authSessionsRepository: Symbol('authSessionsRepository'), accessTokensRepository: Symbol('accessTokensRepository'), signinsRepository: Symbol('signinsRepository'), - messagingMessagesRepository: Symbol('messagingMessagesRepository'), pagesRepository: Symbol('pagesRepository'), pageLikesRepository: Symbol('pageLikesRepository'), galleryPostsRepository: Symbol('galleryPostsRepository'), diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts index ca224971c5..1c6f5776db 100644 --- a/packages/backend/src/misc/emoji-regex.ts +++ b/packages/backend/src/misc/emoji-regex.ts @@ -1,4 +1,4 @@ -import twemoji from 'twemoji-parser/dist/lib/regex.js'; -const twemojiRegex = twemoji.default; +// 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; export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); diff --git a/packages/backend/src/misc/is-not-null.ts b/packages/backend/src/misc/is-not-null.ts new file mode 100644 index 0000000000..d89a1957be --- /dev/null +++ b/packages/backend/src/misc/is-not-null.ts @@ -0,0 +1,5 @@ +// we are using {} as "any non-nullish value" as expected +// eslint-disable-next-line @typescript-eslint/ban-types +export function isNotNull(input: T | undefined | null): input is T { + return input != null; +} diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 7aeb65f296..6a0802f8a4 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -10,7 +10,6 @@ import { import { packedNoteSchema } from '@/models/schema/note.js'; import { packedUserListSchema } from '@/models/schema/user-list.js'; import { packedAppSchema } from '@/models/schema/app.js'; -import { packedMessagingMessageSchema } from '@/models/schema/messaging-message.js'; import { packedNotificationSchema } from '@/models/schema/notification.js'; import { packedDriveFileSchema } from '@/models/schema/drive-file.js'; import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js'; @@ -20,7 +19,6 @@ import { packedBlockingSchema } from '@/models/schema/blocking.js'; import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js'; import { packedHashtagSchema } from '@/models/schema/hashtag.js'; import { packedPageSchema } from '@/models/schema/page.js'; -import { packedUserGroupSchema } from '@/models/schema/user-group.js'; import { packedNoteFavoriteSchema } from '@/models/schema/note-favorite.js'; import { packedChannelSchema } from '@/models/schema/channel.js'; import { packedAntennaSchema } from '@/models/schema/antenna.js'; @@ -28,7 +26,8 @@ import { packedClipSchema } from '@/models/schema/clip.js'; import { packedFederationInstanceSchema } from '@/models/schema/federation-instance.js'; import { packedQueueCountSchema } from '@/models/schema/queue.js'; import { packedGalleryPostSchema } from '@/models/schema/gallery-post.js'; -import { packedEmojiSchema } from '@/models/schema/emoji.js'; +import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/schema/emoji.js'; +import { packedFlashSchema } from '@/models/schema/flash.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -40,9 +39,7 @@ export const refs = { User: packedUserSchema, UserList: packedUserListSchema, - UserGroup: packedUserGroupSchema, App: packedAppSchema, - MessagingMessage: packedMessagingMessageSchema, Note: packedNoteSchema, NoteReaction: packedNoteReactionSchema, NoteFavorite: packedNoteFavoriteSchema, @@ -60,7 +57,9 @@ export const refs = { Clip: packedClipSchema, FederationInstance: packedFederationInstanceSchema, GalleryPost: packedGalleryPostSchema, - Emoji: packedEmojiSchema, + EmojiSimple: packedEmojiSimpleSchema, + EmojiDetailed: packedEmojiDetailedSchema, + Flash: packedFlashSchema, }; export type Packed = SchemaType; @@ -117,10 +116,10 @@ export type Obj = Record; // https://github.com/misskey-dev/misskey/issues/8535 // To avoid excessive stack depth error, // deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it). -export type ObjType = +export type ObjType> = UnionToIntersection< { -readonly [R in RequiredPropertyNames]-?: SchemaType } & - { -readonly [R in RequiredProps]-?: SchemaType } & + { -readonly [R in RequiredProps[number]]-?: SchemaType } & { -readonly [P in keyof s]?: SchemaType } >; @@ -137,18 +136,19 @@ type PartialIntersection = Partial>; // https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 // To get union, we use `Foo extends any ? Hoge : never` type UnionSchemaType = X extends any ? SchemaType : never; -type UnionObjectSchemaType = X extends any ? ObjectSchemaType : never; +//type UnionObjectSchemaType = X extends any ? ObjectSchemaType : never; +type UnionObjType = a[number]> = X extends any ? ObjType : never; type ArrayUnion = T extends any ? Array : never; type ObjectSchemaTypeDef

= p['ref'] extends keyof typeof refs ? Packed : p['properties'] extends NonNullable ? - p['anyOf'] extends ReadonlyArray ? - ObjType[number]> & UnionObjectSchemaType & PartialIntersection> - : - ObjType[number]> + p['anyOf'] extends ReadonlyArray ? p['anyOf'][number]['required'] extends ReadonlyArray ? + UnionObjType> & ObjType> + : never + : ObjType> : - p['anyOf'] extends ReadonlyArray ? UnionObjectSchemaType & PartialIntersection> : + p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md p['allOf'] extends ReadonlyArray ? UnionToIntersection> : any diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 2a235bc6fc..311f875ba5 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js'; +import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -118,24 +118,6 @@ const $userListJoiningsRepository: Provider = { inject: [DI.db], }; -const $userGroupsRepository: Provider = { - provide: DI.userGroupsRepository, - useFactory: (db: DataSource) => db.getRepository(UserGroup), - inject: [DI.db], -}; - -const $userGroupJoiningsRepository: Provider = { - provide: DI.userGroupJoiningsRepository, - useFactory: (db: DataSource) => db.getRepository(UserGroupJoining), - inject: [DI.db], -}; - -const $userGroupInvitationsRepository: Provider = { - provide: DI.userGroupInvitationsRepository, - useFactory: (db: DataSource) => db.getRepository(UserGroupInvitation), - inject: [DI.db], -}; - const $userNotePiningsRepository: Provider = { provide: DI.userNotePiningsRepository, useFactory: (db: DataSource) => db.getRepository(UserNotePining), @@ -256,12 +238,6 @@ const $signinsRepository: Provider = { inject: [DI.db], }; -const $messagingMessagesRepository: Provider = { - provide: DI.messagingMessagesRepository, - useFactory: (db: DataSource) => db.getRepository(MessagingMessage), - inject: [DI.db], -}; - const $pagesRepository: Provider = { provide: DI.pagesRepository, useFactory: (db: DataSource) => db.getRepository(Page), @@ -435,9 +411,6 @@ const $roleAssignmentsRepository: Provider = { $userPublickeysRepository, $userListsRepository, $userListJoiningsRepository, - $userGroupsRepository, - $userGroupJoiningsRepository, - $userGroupInvitationsRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, @@ -458,7 +431,6 @@ const $roleAssignmentsRepository: Provider = { $authSessionsRepository, $accessTokensRepository, $signinsRepository, - $messagingMessagesRepository, $pagesRepository, $pageLikesRepository, $galleryPostsRepository, @@ -505,9 +477,6 @@ const $roleAssignmentsRepository: Provider = { $userPublickeysRepository, $userListsRepository, $userListJoiningsRepository, - $userGroupsRepository, - $userGroupJoiningsRepository, - $userGroupInvitationsRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, @@ -528,7 +497,6 @@ const $roleAssignmentsRepository: Provider = { $authSessionsRepository, $accessTokensRepository, $signinsRepository, - $messagingMessagesRepository, $pagesRepository, $pageLikesRepository, $galleryPostsRepository, diff --git a/packages/backend/src/models/entities/Ad.ts b/packages/backend/src/models/entities/Ad.ts index 36b758f205..56baf863ca 100644 --- a/packages/backend/src/models/entities/Ad.ts +++ b/packages/backend/src/models/entities/Ad.ts @@ -18,6 +18,13 @@ export class Ad { }) public expiresAt: Date; + @Index() + @Column('timestamp with time zone', { + comment: 'The expired date of the Ad.', + default: () => 'now()', + }) + public startsAt: Date; + @Column('varchar', { length: 32, nullable: false, }) diff --git a/packages/backend/src/models/entities/Antenna.ts b/packages/backend/src/models/entities/Antenna.ts index 860fd9cf55..5b2164ef17 100644 --- a/packages/backend/src/models/entities/Antenna.ts +++ b/packages/backend/src/models/entities/Antenna.ts @@ -2,7 +2,6 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ import { id } from '../id.js'; import { User } from './User.js'; import { UserList } from './UserList.js'; -import { UserGroupJoining } from './UserGroupJoining.js'; @Entity() export class Antenna { @@ -33,8 +32,8 @@ export class Antenna { }) public name: string; - @Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] }) - public src: 'home' | 'all' | 'users' | 'list' | 'group'; + @Column('enum', { enum: ['home', 'all', 'users', 'list'] }) + public src: 'home' | 'all' | 'users' | 'list'; @Column({ ...id(), @@ -48,18 +47,6 @@ export class Antenna { @JoinColumn() public userList: UserList | null; - @Column({ - ...id(), - nullable: true, - }) - public userGroupJoiningId: UserGroupJoining['id'] | null; - - @ManyToOne(type => UserGroupJoining, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroupJoining: UserGroupJoining | null; - @Column('varchar', { length: 1024, array: true, default: '{}', diff --git a/packages/backend/src/models/entities/Flash.ts b/packages/backend/src/models/entities/Flash.ts index 07039d4fa1..192f7e7bc4 100644 --- a/packages/backend/src/models/entities/Flash.ts +++ b/packages/backend/src/models/entities/Flash.ts @@ -1,7 +1,6 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; import { id } from '../id.js'; import { User } from './User.js'; -import { DriveFile } from './DriveFile.js'; @Entity() export class Flash { diff --git a/packages/backend/src/models/entities/MessagingMessage.ts b/packages/backend/src/models/entities/MessagingMessage.ts deleted file mode 100644 index 69fc9815d4..0000000000 --- a/packages/backend/src/models/entities/MessagingMessage.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { DriveFile } from './DriveFile.js'; -import { UserGroup } from './UserGroup.js'; - -@Entity() -export class MessagingMessage { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the MessagingMessage.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The sender user ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), nullable: true, - comment: 'The recipient user ID.', - }) - public recipientId: User['id'] | null; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public recipient: User | null; - - @Index() - @Column({ - ...id(), nullable: true, - comment: 'The recipient group ID.', - }) - public groupId: UserGroup['id'] | null; - - @ManyToOne(type => UserGroup, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public group: UserGroup | null; - - @Column('varchar', { - length: 4096, nullable: true, - }) - public text: string | null; - - @Column('boolean', { - default: false, - }) - public isRead: boolean; - - @Column('varchar', { - length: 512, nullable: true, - }) - public uri: string | null; - - @Column({ - ...id(), - array: true, default: '{}', - }) - public reads: User['id'][]; - - @Column({ - ...id(), - nullable: true, - }) - public fileId: DriveFile['id'] | null; - - @ManyToOne(type => DriveFile, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public file: DriveFile | null; -} diff --git a/packages/backend/src/models/entities/NoteThreadMuting.ts b/packages/backend/src/models/entities/NoteThreadMuting.ts index a23176b994..3c884fe615 100644 --- a/packages/backend/src/models/entities/NoteThreadMuting.ts +++ b/packages/backend/src/models/entities/NoteThreadMuting.ts @@ -1,7 +1,6 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; import { id } from '../id.js'; import { User } from './User.js'; -import { Note } from './Note.js'; @Entity() @Index(['userId', 'threadId'], { unique: true }) diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts index 66f131d1c0..51117efba5 100644 --- a/packages/backend/src/models/entities/Notification.ts +++ b/packages/backend/src/models/entities/Notification.ts @@ -1,10 +1,9 @@ import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm'; -import { notificationTypes } from '@/types.js'; +import { notificationTypes, obsoleteNotificationTypes } from '@/types.js'; import { id } from '../id.js'; import { User } from './User.js'; import { Note } from './Note.js'; import { FollowRequest } from './FollowRequest.js'; -import { UserGroupInvitation } from './UserGroupInvitation.js'; import { AccessToken } from './AccessToken.js'; @Entity() @@ -59,17 +58,18 @@ export class Notification { * renote - 投稿がRenoteされた * quote - 投稿が引用Renoteされた * reaction - 投稿にリアクションされた - * pollVote - 投稿のアンケートに投票された (廃止) * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された - * groupInvited - グループに招待された * achievementEarned - 実績を獲得 * app - アプリ通知 */ @Index() @Column('enum', { - enum: notificationTypes, + enum: [ + ...notificationTypes, + ...obsoleteNotificationTypes, + ], comment: 'The type of the Notification.', }) public type: typeof notificationTypes[number]; @@ -108,18 +108,6 @@ export class Notification { @JoinColumn() public followRequest: FollowRequest | null; - @Column({ - ...id(), - nullable: true, - }) - public userGroupInvitationId: UserGroupInvitation['id'] | null; - - @ManyToOne(type => UserGroupInvitation, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroupInvitation: UserGroupInvitation | null; - @Column('varchar', { length: 128, nullable: true, }) diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts index 8cf6811863..399e9ead05 100644 --- a/packages/backend/src/models/entities/Role.ts +++ b/packages/backend/src/models/entities/Role.ts @@ -1,4 +1,4 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { Entity, Column, PrimaryColumn } from 'typeorm'; import { id } from '../id.js'; type CondFormulaValueAnd = { diff --git a/packages/backend/src/models/entities/RoleAssignment.ts b/packages/backend/src/models/entities/RoleAssignment.ts index e86f2a8999..972810940f 100644 --- a/packages/backend/src/models/entities/RoleAssignment.ts +++ b/packages/backend/src/models/entities/RoleAssignment.ts @@ -39,4 +39,10 @@ export class RoleAssignment { }) @JoinColumn() public role: Role | null; + + @Index() + @Column('timestamp with time zone', { + nullable: true, + }) + public expiresAt: Date | null; } diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts index 1cfcc814ea..0a8b89ea06 100644 --- a/packages/backend/src/models/entities/User.ts +++ b/packages/backend/src/models/entities/User.ts @@ -215,20 +215,16 @@ export class User { } } -export interface ILocalUser extends User { +export type LocalUser = User & { host: null; + uri: null; } -export interface IRemoteUser extends User { +export type RemoteUser = User & { host: string; + uri: string; } -export type CacheableLocalUser = ILocalUser; - -export type CacheableRemoteUser = IRemoteUser; - -export type CacheableUser = CacheableLocalUser | CacheableRemoteUser; - export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; export const passwordSchema = { type: 'string', minLength: 1 } as const; export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; diff --git a/packages/backend/src/models/entities/UserGroup.ts b/packages/backend/src/models/entities/UserGroup.ts deleted file mode 100644 index 328a1883cb..0000000000 --- a/packages/backend/src/models/entities/UserGroup.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; - -@Entity() -export class UserGroup { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the UserGroup.', - }) - public createdAt: Date; - - @Column('varchar', { - length: 256, - }) - public name: string; - - @Index() - @Column({ - ...id(), - comment: 'The ID of owner.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('boolean', { - default: false, - }) - public isPrivate: boolean; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} diff --git a/packages/backend/src/models/entities/UserGroupInvitation.ts b/packages/backend/src/models/entities/UserGroupInvitation.ts deleted file mode 100644 index e4aa3ccae1..0000000000 --- a/packages/backend/src/models/entities/UserGroupInvitation.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { UserGroup } from './UserGroup.js'; - -@Entity() -@Index(['userId', 'userGroupId'], { unique: true }) -export class UserGroupInvitation { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserGroupInvitation.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The group ID.', - }) - public userGroupId: UserGroup['id']; - - @ManyToOne(type => UserGroup, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroup: UserGroup | null; -} diff --git a/packages/backend/src/models/entities/UserGroupJoining.ts b/packages/backend/src/models/entities/UserGroupJoining.ts deleted file mode 100644 index fae7241525..0000000000 --- a/packages/backend/src/models/entities/UserGroupJoining.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { UserGroup } from './UserGroup.js'; - -@Entity() -@Index(['userId', 'userGroupId'], { unique: true }) -export class UserGroupJoining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserGroupJoining.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The group ID.', - }) - public userGroupId: UserGroup['id']; - - @ManyToOne(type => UserGroup, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroup: UserGroup | null; -} diff --git a/packages/backend/src/models/entities/UserIp.ts b/packages/backend/src/models/entities/UserIp.ts index e9afd40d49..628e3d0361 100644 --- a/packages/backend/src/models/entities/UserIp.ts +++ b/packages/backend/src/models/entities/UserIp.ts @@ -1,6 +1,5 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Entity, Index, Column, PrimaryGeneratedColumn } from 'typeorm'; import { id } from '../id.js'; -import { Note } from './Note.js'; import type { User } from './User.js'; @Entity() diff --git a/packages/backend/src/models/entities/UserProfile.ts b/packages/backend/src/models/entities/UserProfile.ts index 1ff261cda3..60c1c55de5 100644 --- a/packages/backend/src/models/entities/UserProfile.ts +++ b/packages/backend/src/models/entities/UserProfile.ts @@ -1,5 +1,5 @@ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { ffVisibility, notificationTypes } from '@/types.js'; +import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js'; import { id } from '../id.js'; import { User } from './User.js'; import { Page } from './Page.js'; @@ -71,7 +71,7 @@ export class UserProfile { public emailVerified: boolean; @Column('jsonb', { - default: ['follow', 'receiveFollowRequest', 'groupInvited'], + default: ['follow', 'receiveFollowRequest'], }) public emailNotificationTypes: string[]; @@ -202,7 +202,11 @@ export class UserProfile { public mutedInstances: string[]; @Column('enum', { - enum: notificationTypes, + enum: [ + ...notificationTypes, + // マイグレーションで削除が困難なので古いenumは残しておく + ...obsoleteNotificationTypes, + ], array: true, default: [], }) diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 50697597ad..25ed9b89d8 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -22,7 +22,6 @@ import { GalleryLike } from '@/models/entities/GalleryLike.js'; import { GalleryPost } from '@/models/entities/GalleryPost.js'; import { Hashtag } from '@/models/entities/Hashtag.js'; import { Instance } from '@/models/entities/Instance.js'; -import { MessagingMessage } from '@/models/entities/MessagingMessage.js'; import { Meta } from '@/models/entities/Meta.js'; import { ModerationLog } from '@/models/entities/ModerationLog.js'; import { MutedNote } from '@/models/entities/MutedNote.js'; @@ -47,9 +46,6 @@ import { Signin } from '@/models/entities/Signin.js'; import { SwSubscription } from '@/models/entities/SwSubscription.js'; import { UsedUsername } from '@/models/entities/UsedUsername.js'; import { User } from '@/models/entities/User.js'; -import { UserGroup } from '@/models/entities/UserGroup.js'; -import { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; -import { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; import { UserIp } from '@/models/entities/UserIp.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UserList } from '@/models/entities/UserList.js'; @@ -93,7 +89,6 @@ export { GalleryPost, Hashtag, Instance, - MessagingMessage, Meta, ModerationLog, MutedNote, @@ -118,9 +113,6 @@ export { SwSubscription, UsedUsername, User, - UserGroup, - UserGroupInvitation, - UserGroupJoining, UserIp, UserKeypair, UserList, @@ -163,7 +155,6 @@ export type GalleryLikesRepository = Repository; export type GalleryPostsRepository = Repository; export type HashtagsRepository = Repository; export type InstancesRepository = Repository; -export type MessagingMessagesRepository = Repository; export type MetasRepository = Repository; export type ModerationLogsRepository = Repository; export type MutedNotesRepository = Repository; @@ -188,9 +179,6 @@ export type SigninsRepository = Repository; export type SwSubscriptionsRepository = Repository; export type UsedUsernamesRepository = Repository; export type UsersRepository = Repository; -export type UserGroupsRepository = Repository; -export type UserGroupInvitationsRepository = Repository; -export type UserGroupJoiningsRepository = Repository; export type UserIpsRepository = Repository; export type UserKeypairsRepository = Repository; export type UserListsRepository = Repository; diff --git a/packages/backend/src/models/schema/antenna.ts b/packages/backend/src/models/schema/antenna.ts index 9cf522802c..f0994e48f7 100644 --- a/packages/backend/src/models/schema/antenna.ts +++ b/packages/backend/src/models/schema/antenna.ts @@ -42,18 +42,13 @@ export const packedAntennaSchema = { src: { type: 'string', optional: false, nullable: false, - enum: ['home', 'all', 'users', 'list', 'group'], + enum: ['home', 'all', 'users', 'list'], }, userListId: { type: 'string', optional: false, nullable: true, format: 'id', }, - userGroupId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, users: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/schema/emoji.ts index 143f25373c..c00c3dac1d 100644 --- a/packages/backend/src/models/schema/emoji.ts +++ b/packages/backend/src/models/schema/emoji.ts @@ -1,11 +1,37 @@ -export const packedEmojiSchema = { +export const packedEmojiSimpleSchema = { + type: 'object', + properties: { + aliases: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + category: { + type: 'string', + optional: false, nullable: true, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + }, +} as const; + +export const packedEmojiDetailedSchema = { type: 'object', properties: { id: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, format: 'id', - example: 'xxxxxxxxxx', }, aliases: { type: 'array', @@ -26,12 +52,12 @@ export const packedEmojiSchema = { }, host: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, description: 'The local host is represented with `null`.', }, url: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, }, } as const; diff --git a/packages/backend/src/models/schema/flash.ts b/packages/backend/src/models/schema/flash.ts new file mode 100644 index 0000000000..8471a138ec --- /dev/null +++ b/packages/backend/src/models/schema/flash.ts @@ -0,0 +1,51 @@ +export const packedFlashSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + updatedAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + title: { + type: 'string', + optional: false, nullable: false, + }, + summary: { + type: 'string', + optional: false, nullable: false, + }, + script: { + 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, + }, + isLiked: { + type: 'boolean', + optional: true, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/schema/messaging-message.ts b/packages/backend/src/models/schema/messaging-message.ts deleted file mode 100644 index b1ffa45955..0000000000 --- a/packages/backend/src/models/schema/messaging-message.ts +++ /dev/null @@ -1,73 +0,0 @@ -export const packedMessagingMessageSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user: { - type: 'object', - ref: 'UserLite', - optional: true, nullable: false, - }, - text: { - type: 'string', - optional: false, nullable: true, - }, - fileId: { - type: 'string', - optional: true, nullable: true, - format: 'id', - }, - file: { - type: 'object', - optional: true, nullable: true, - ref: 'DriveFile', - }, - recipientId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, - recipient: { - type: 'object', - optional: true, nullable: true, - ref: 'UserLite', - }, - groupId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, - group: { - type: 'object', - optional: true, nullable: true, - ref: 'UserGroup', - }, - isRead: { - type: 'boolean', - optional: true, nullable: false, - }, - reads: { - type: 'array', - optional: true, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/user-group.ts b/packages/backend/src/models/schema/user-group.ts deleted file mode 100644 index a73bf82bb8..0000000000 --- a/packages/backend/src/models/schema/user-group.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const packedUserGroupSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - name: { - type: 'string', - optional: false, nullable: false, - }, - ownerId: { - type: 'string', - nullable: false, optional: false, - format: 'id', - }, - userIds: { - type: 'array', - nullable: false, optional: true, - items: { - type: 'string', - nullable: false, optional: false, - format: 'id', - }, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts index 1fc9352539..c390018b46 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/schema/user.ts @@ -311,10 +311,6 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, - hasUnreadMessagingMessage: { - type: 'boolean', - nullable: false, optional: false, - }, hasUnreadNotification: { type: 'boolean', nullable: false, optional: false, diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 33b924e776..c2ee14b0f4 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -30,7 +30,6 @@ import { GalleryLike } from '@/models/entities/GalleryLike.js'; import { GalleryPost } from '@/models/entities/GalleryPost.js'; import { Hashtag } from '@/models/entities/Hashtag.js'; import { Instance } from '@/models/entities/Instance.js'; -import { MessagingMessage } from '@/models/entities/MessagingMessage.js'; import { Meta } from '@/models/entities/Meta.js'; import { ModerationLog } from '@/models/entities/ModerationLog.js'; import { MutedNote } from '@/models/entities/MutedNote.js'; @@ -55,9 +54,6 @@ import { Signin } from '@/models/entities/Signin.js'; import { SwSubscription } from '@/models/entities/SwSubscription.js'; import { UsedUsername } from '@/models/entities/UsedUsername.js'; import { User } from '@/models/entities/User.js'; -import { UserGroup } from '@/models/entities/UserGroup.js'; -import { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; -import { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; import { UserIp } from '@/models/entities/UserIp.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UserList } from '@/models/entities/UserList.js'; @@ -78,7 +74,6 @@ import { FlashLike } from '@/models/entities/FlashLike.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { envOption } from './env.js'; export const dbLogger = new MisskeyLogger('db'); @@ -137,9 +132,6 @@ export const entities = [ UserPublickey, UserList, UserListJoining, - UserGroup, - UserGroupJoining, - UserGroupInvitation, UserNotePining, UserSecurityKey, UsedUsername, @@ -167,7 +159,6 @@ export const entities = [ SwSubscription, AbuseUserReport, RegistrationTicket, - MessagingMessage, Signin, ModerationLog, Clip, diff --git a/packages/backend/src/queue/DbQueueProcessorsService.ts b/packages/backend/src/queue/DbQueueProcessorsService.ts index df337ad810..910e2bea45 100644 --- a/packages/backend/src/queue/DbQueueProcessorsService.ts +++ b/packages/backend/src/queue/DbQueueProcessorsService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { DbJobData } from '@/queue/types.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts b/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts index c95e1c1ba6..865e47c3f8 100644 --- a/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts +++ b/packages/backend/src/queue/ObjectStorageQueueProcessorsService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { ObjectStorageJobData } from '@/queue/types.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; diff --git a/packages/backend/src/queue/QueueLoggerService.ts b/packages/backend/src/queue/QueueLoggerService.ts index 3a8a734f10..648af893c2 100644 --- a/packages/backend/src/queue/QueueLoggerService.ts +++ b/packages/backend/src/queue/QueueLoggerService.ts @@ -1,7 +1,6 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class QueueLoggerService { diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 2123815c4c..8457747cb0 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts index da4ae88557..02324c6cd4 100644 --- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts +++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { In, IsNull, MoreThan } from 'typeorm'; +import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index 7a1e3e71be..f4cd560fc9 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { In, MoreThan } from 'typeorm'; +import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MutingsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index 5254d3c7d8..b458167042 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { In, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 4aa1dc15fd..7fd2cde9c0 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; -import { In, LessThan, MoreThan } from 'typeorm'; +import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, UserIpsRepository } from '@/models/index.js'; +import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; @@ -29,6 +29,9 @@ export class CleanProcessorService { @Inject(DI.antennaNotesRepository) private antennaNotesRepository: AntennaNotesRepository, + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + private queueLoggerService: QueueLoggerService, private idService: IdService, ) { @@ -56,6 +59,17 @@ export class CleanProcessorService { id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), }); + const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign') + .where('assign.expiresAt IS NOT NULL') + .andWhere('assign.expiresAt < :now', { now: new Date() }) + .getMany(); + + if (expiredRoleAssignments.length > 0) { + await this.roleAssignmentsRepository.delete({ + id: In(expiredRoleAssignments.map(x => x.id)), + }); + } + this.logger.succ('Cleaned.'); done(); } diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 10fcb5684f..2a053a12e0 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 21d2dc9efc..037dfa1a53 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { PollVotesRepository, NotesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index 5b3c1a415b..7f2c2d08b5 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, BlockingsRepository, DriveFilesRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js'; +import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index df024a8f3c..b50f373ef8 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -1,8 +1,7 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; +import { IsNull } from 'typeorm'; import { format as dateFormat } from 'date-fns'; -import { ulid } from 'ulid'; import mime from 'mime-types'; import archiver from 'archiver'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index 3820705e5c..c65f0a97a0 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; +import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; import type { NoteFavorite, NoteFavoritesRepository, NotesRepository, PollsRepository, User, UsersRepository } from '@/models/index.js'; diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 8431829e91..3f4f16a2ec 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; +import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, PollsRepository, UsersRepository } from '@/models/index.js'; diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index a8daa5e5ee..6400161b8c 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; -import { In, IsNull, MoreThan } from 'typeorm'; +import { In } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/index.js'; diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index 2eed420e96..b8d9b3a52d 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; +import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, BlockingsRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 2d43615e25..4ecf8daf74 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan, DataSource } from 'typeorm'; +import { DataSource } from 'typeorm'; import unzipper from 'unzipper'; import { DI } from '@/di-symbols.js'; import type { EmojisRepository, DriveFilesRepository, UsersRepository } from '@/models/index.js'; diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index b61846d747..037a6f2456 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; +import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index 21236da2ef..83d382057b 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; +import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index a9672250c8..c423863410 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; +import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index f814368a7a..33d6f4eafa 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -1,6 +1,5 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; import httpSignature from '@peertube/http-signature'; import { DI } from '@/di-symbols.js'; import type { InstancesRepository, DriveFilesRepository } from '@/models/index.js'; @@ -10,13 +9,11 @@ import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { Cache } from '@/misc/cache.js'; -import type { Instance } from '@/models/entities/Instance.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import FederationChart from '@/core/chart/charts/federation.js'; import { getApId } from '@/core/activitypub/type.js'; -import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { StatusError } from '@/misc/status-error.js'; @@ -27,7 +24,7 @@ import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; -import type { DeliverJobData, InboxJobData } from '../types.js'; +import type { InboxJobData } from '../types.js'; // ユーザーのinboxにアクティビティが届いた時の処理 @Injectable() @@ -87,7 +84,7 @@ export class InboxProcessorService { // HTTP-Signature keyIdを元にDBから取得 let authUser: { - user: CacheableRemoteUser; + user: RemoteUser; key: UserPublickey | null; } | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId); diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index 74e7c632d5..e5840f3da8 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { In, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index 751e02dc20..7ff84c15a5 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { In, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index 57210b25d2..39b1b95658 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { WebhooksRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 186d3822d8..da8d0114e5 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -11,7 +11,7 @@ import * as url from '@/misc/prelude/url.js'; import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; import type { Following } from '@/models/entities/Following.js'; import { countIf } from '@/misc/prelude/array.js'; @@ -183,13 +183,13 @@ export class ActivityPubServerService { ); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } else { // index page const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } } @@ -271,13 +271,13 @@ export class ActivityPubServerService { ); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } else { // index page const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } } @@ -312,7 +312,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } @bindThis @@ -389,7 +389,7 @@ export class ActivityPubServerService { ); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } else { // index page const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount, @@ -398,7 +398,7 @@ export class ActivityPubServerService { ); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } } @@ -411,7 +411,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.apRendererService.renderPerson(user as ILocalUser))); + return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as LocalUser))); } @bindThis @@ -441,6 +441,14 @@ export class ActivityPubServerService { fastify.addContentTypeParser('application/activity+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore')); fastify.addContentTypeParser('application/ld+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore')); + fastify.addHook('onRequest', (request, reply, done) => { + reply.header('Access-Control-Allow-Headers', 'Accept'); + reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS'); + reply.header('Access-Control-Allow-Origin', '*'); + reply.header('Access-Control-Expose-Headers', 'Vary'); + done(); + }); + //#region Routing // inbox (limit: 64kb) fastify.post('/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply)); @@ -473,7 +481,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.apRendererService.renderNote(note, false))); + return this.apRendererService.addContext(await this.apRendererService.renderNote(note, false)); }); // note activity @@ -494,7 +502,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.packActivity(note))); + return (this.apRendererService.addContext(await this.packActivity(note))); }); // outbox @@ -537,7 +545,7 @@ export class ActivityPubServerService { if (this.userEntityService.isLocalUser(user)) { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair))); + return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair))); } else { reply.code(400); return; @@ -581,7 +589,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.apRendererService.renderEmoji(emoji))); + return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji))); }); // like @@ -602,7 +610,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.apRendererService.renderLike(reaction, note))); + return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note))); }); // follow @@ -628,7 +636,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee))); + return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); }); done(); diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 4bd6d0f556..e5eefac1fa 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -150,6 +150,12 @@ export class FileServerService { file.cleanup(); return await reply.redirect(301, url.toString()); } else if (file.mime.startsWith('video/')) { + const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url); + if (externalThumbnail) { + file.cleanup(); + return await reply.redirect(301, externalThumbnail); + } + image = await this.videoProcessingService.generateVideoThumbnail(file.path); } } @@ -220,7 +226,10 @@ export class FileServerService { return; } - if (this.config.externalMediaProxyEnabled) { + // アバタークロップなど、どうしてもオリジンである必要がある場合 + const mustOrigin = 'origin' in request.query; + + if (this.config.externalMediaProxyEnabled && !mustOrigin) { // 外部のメディアプロキシが有効なら、そちらにリダイレクト reply.header('Cache-Control', 'public, max-age=259200'); // 3 days @@ -255,8 +264,21 @@ export class FileServerService { const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image'); const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image'); + if ( + 'emoji' in request.query || + 'avatar' in request.query || + 'static' in request.query || + 'preview' in request.query || + 'badge' in request.query + ) { + if (!isConvertibleImage) { + // 画像でないなら404でお茶を濁す + throw new StatusError('Unexpected mime', 404); + } + } + let image: IImageStreamable | null = null; - if (('emoji' in request.query || 'avatar' in request.query) && isConvertibleImage) { + if ('emoji' in request.query || 'avatar' in request.query) { if (!isAnimationConvertibleImage && !('static' in request.query)) { image = { data: fs.createReadStream(file.path), @@ -277,16 +299,11 @@ export class FileServerService { type: 'image/webp', }; } - } else if ('static' in request.query && isConvertibleImage) { + } else if ('static' in request.query) { image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280); - } else if ('preview' in request.query && isConvertibleImage) { + } else if ('preview' in request.query) { image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200); } else if ('badge' in request.query) { - if (!isConvertibleImage) { - // 画像でないなら404でお茶を濁す - throw new StatusError('Unexpected mime', 404); - } - const mask = sharp(file.path) .resize(96, 96, { fit: 'inside', @@ -381,7 +398,7 @@ export class FileServerService { state: 'remote', mime, ext, path, cleanup, - } + }; } catch (e) { cleanup(); throw e; @@ -415,7 +432,7 @@ export class FileServerService { url: file.uri, fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', file, - } + }; } const path = this.internalStorageService.resolvePath(key); @@ -438,6 +455,6 @@ export class FileServerService { mime: file.type, ext: null, path, - } + }; } } diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index a43630c041..00a0d93093 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index b605f3c8ab..a5a5f9e7f9 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -30,8 +30,6 @@ 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'; import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js'; -import { MessagingIndexChannelService } from './api/stream/channels/messaging-index.js'; -import { MessagingChannelService } from './api/stream/channels/messaging.js'; import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { UserListChannelService } from './api/stream/channels/user-list.js'; @@ -71,8 +69,6 @@ import { UserListChannelService } from './api/stream/channels/user-list.js'; HomeTimelineChannelService, HybridTimelineChannelService, LocalTimelineChannelService, - MessagingIndexChannelService, - MessagingChannelService, QueueStatsChannelService, ServerStatsChannelService, UserListChannelService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index f76871c60f..e61383468c 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -1,14 +1,13 @@ import cluster from 'node:cluster'; import * as fs from 'node:fs'; -import { Inject, Injectable } from '@nestjs/common'; -import Fastify from 'fastify'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import Fastify, { FastifyInstance } from 'fastify'; import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Config } from '@/config.js'; import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; -import { envOption } from '@/env.js'; import * as Acct from '@/misc/acct.js'; import { genIdenticon } from '@/misc/gen-identicon.js'; import { createTemp } from '@/misc/create-temp.js'; @@ -24,8 +23,9 @@ import { FileServerService } from './FileServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; @Injectable() -export class ServerService { +export class ServerService implements OnApplicationShutdown { private logger: Logger; + #fastify: FastifyInstance; constructor( @Inject(DI.config) @@ -55,11 +55,12 @@ export class ServerService { } @bindThis - public launch() { + public async launch() { const fastify = Fastify({ trustProxy: true, logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''), }); + this.#fastify = fastify; // HSTS // 6months (15552000sec) @@ -76,7 +77,7 @@ export class ServerService { fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.wellKnownServerService.createServer); - fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { + fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { const path = request.params.path; reply.header('Cache-Control', 'public, max-age=86400'); @@ -106,11 +107,19 @@ export class ServerService { } } - const url = new URL(`${this.config.mediaProxy}/emoji.webp`); - // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) - url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); - url.searchParams.set('emoji', '1'); - if ('static' in request.query) url.searchParams.set('static', '1'); + let url: URL; + if ('badge' in request.query) { + url = new URL(`${this.config.mediaProxy}/emoji.png`); + // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) + url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); + url.searchParams.set('badge', '1'); + } else { + url = new URL(`${this.config.mediaProxy}/emoji.webp`); + // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) + url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); + url.searchParams.set('emoji', '1'); + if ('static' in request.query) url.searchParams.set('static', '1'); + } return await reply.redirect( 301, @@ -196,5 +205,11 @@ export class ServerService { }); fastify.listen({ port: this.config.port, host: '0.0.0.0' }); + + await fastify.ready(); + } + + async onApplicationShutdown(signal: string): Promise { + await this.#fastify.close(); } } diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index 9bfd216ccb..e722563036 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, MoreThan } from 'typeorm'; +import { IsNull } from 'typeorm'; import vary from 'vary'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/index.js'; diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 395a1c468a..f84a3aa59b 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -1,11 +1,11 @@ -import { performance } from 'perf_hooks'; import { pipeline } from 'node:stream'; import * as fs from 'node:fs'; import { promisify } from 'node:util'; import { Inject, Injectable } from '@nestjs/common'; +import { v4 as uuid } from 'uuid'; import { DI } from '@/di-symbols.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; -import type { CacheableLocalUser, ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import type Logger from '@/logger.js'; import type { UserIpsRepository } from '@/models/index.js'; @@ -100,18 +100,21 @@ export class ApiCallService implements OnApplicationShutdown { request: FastifyRequest<{ Body: Record, Querystring: Record }>, reply: FastifyReply, ) { - const multipartData = await request.file(); + const multipartData = await request.file().catch(() => { + /* Fastify throws if the remote didn't send multipart data. Return 400 below. */ + }); if (multipartData == null) { reply.code(400); + reply.send(); return; } const [path] = await createTemp(); await pump(multipartData.file, fs.createWriteStream(path)); - const fields = {} as Record; + const fields = {} as Record; for (const [k, v] of Object.entries(multipartData.fields)) { - fields[k] = v.value; + fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined; } const token = fields['i']; @@ -168,7 +171,7 @@ export class ApiCallService implements OnApplicationShutdown { } @bindThis - private async logIp(request: FastifyRequest, user: ILocalUser) { + private async logIp(request: FastifyRequest, user: LocalUser) { const meta = await this.metaService.fetch(); if (!meta.enableIpLogging) return; const ip = request.ip; @@ -194,7 +197,7 @@ export class ApiCallService implements OnApplicationShutdown { @bindThis private async call( ep: IEndpoint & { exec: any }, - user: CacheableLocalUser | null | undefined, + user: LocalUser | null | undefined, token: AccessToken | null | undefined, data: any, file: { @@ -220,22 +223,24 @@ export class ApiCallService implements OnApplicationShutdown { const limit = Object.assign({}, ep.meta.limit); - if (!limit.key) { - limit.key = ep.name; + if (limit.key == null) { + (limit as any).key = ep.name; } // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1; - // Rate limit - await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, + if (factor > 0) { + // Rate limit + await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }); }); - }); + } } if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) { @@ -319,6 +324,7 @@ export class ApiCallService implements OnApplicationShutdown { if (err instanceof ApiError) { throw err; } else { + const errId = uuid(); this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { ep: ep.name, ps: data, @@ -326,14 +332,15 @@ export class ApiCallService implements OnApplicationShutdown { message: err.message, code: err.name, stack: err.stack, + id: errId, }, }); - console.error(err); + console.error(err, errId); throw new ApiError(null, { e: { message: err.message, code: err.name, - stack: err.stack, + id: errId, }, }); } diff --git a/packages/backend/src/server/api/ApiLoggerService.ts b/packages/backend/src/server/api/ApiLoggerService.ts index cabd65fd3e..7f534b1efd 100644 --- a/packages/backend/src/server/api/ApiLoggerService.ts +++ b/packages/backend/src/server/api/ApiLoggerService.ts @@ -1,7 +1,6 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApiLoggerService { diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index e406949cd4..115d60986c 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -2,13 +2,13 @@ import { Inject, Injectable } from '@nestjs/common'; import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; import fastifyCookie from '@fastify/cookie'; -import { ModuleRef, repl } from '@nestjs/core'; +import { ModuleRef } from '@nestjs/core'; import type { Config } from '@/config.js'; import type { UsersRepository, InstancesRepository, AccessTokensRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import endpoints, { IEndpoint } from './endpoints.js'; +import endpoints from './endpoints.js'; import { ApiCallService } from './ApiCallService.js'; import { SignupApiService } from './SignupApiService.js'; import { SigninApiService } from './SigninApiService.js'; @@ -73,28 +73,32 @@ export class ApiServerService { Params: { endpoint: string; }, Body: Record, Querystring: Record, - }>('/' + endpoint.name, (request, reply) => { + }>('/' + endpoint.name, async (request, reply) => { if (request.method === 'GET' && !endpoint.meta.allowGet) { reply.code(405); reply.send(); return; } - - this.apiCallService.handleMultipartRequest(ep, request, reply); + + // Await so that any error can automatically be translated to HTTP 500 + await this.apiCallService.handleMultipartRequest(ep, request, reply); + return reply; }); } else { fastify.all<{ Params: { endpoint: string; }, Body: Record, Querystring: Record, - }>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, (request, reply) => { + }>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, async (request, reply) => { if (request.method === 'GET' && !endpoint.meta.allowGet) { reply.code(405); reply.send(); return; } - - this.apiCallService.handleRequest(ep, request, reply); + + // Await so that any error can automatically be translated to HTTP 500 + await this.apiCallService.handleRequest(ep, request, reply); + return reply; }); } } @@ -160,6 +164,22 @@ export class ApiServerService { } }); + // Make sure any unknown path under /api returns HTTP 404 Not Found, + // because otherwise ClientServerService will return the base client HTML + // page with HTTP 200. + fastify.get('*', (request, reply) => { + reply.code(404); + // Mock ApiCallService.send's error handling + reply.send({ + error: { + message: 'Unknown API endpoint.', + code: 'UNKNOWN_API_ENDPOINT', + id: '2ca3b769-540a-4f08-9dd5-b5a825b6d0f1', + kind: 'client', + }, + }); + }); + done(); } } diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 8b39f6c924..87438c348d 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; -import type { CacheableLocalUser, ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import { Cache } from '@/misc/cache.js'; import type { App } from '@/models/entities/App.js'; @@ -36,14 +36,14 @@ export class AuthenticateService { } @bindThis - public async authenticate(token: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> { + public async authenticate(token: string | null | undefined): Promise<[LocalUser | null | undefined, AccessToken | null | undefined]> { if (token == null) { return [null, null]; } if (isNativeToken(token)) { const user = await this.userCacheService.localUserByNativeTokenCache.fetch(token, - () => this.usersRepository.findOneBy({ token }) as Promise); + () => this.usersRepository.findOneBy({ token }) as Promise); if (user == null) { throw new AuthenticationError('user not found'); @@ -70,7 +70,7 @@ export class AuthenticateService { const user = await this.userCacheService.localUserByIdCache.fetch(accessToken.userId, () => this.usersRepository.findOneBy({ id: accessToken.userId, - }) as Promise); + }) as Promise); if (accessToken.appId) { const app = await this.appCache.fetch(accessToken.appId, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 4a55c6cbe3..d3e2219bd5 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -66,6 +66,7 @@ import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; +import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -170,6 +171,7 @@ import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js'; import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js'; import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js'; import * as ep___i_2fa_register from './endpoints/i/2fa/register.js'; +import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js'; import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; import * as ep___i_apps from './endpoints/i/apps.js'; @@ -195,7 +197,6 @@ import * as ep___i_notifications from './endpoints/i/notifications.js'; import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; import * as ep___i_pages from './endpoints/i/pages.js'; import * as ep___i_pin from './endpoints/i/pin.js'; -import * as ep___i_readAllMessagingMessages from './endpoints/i/read-all-messaging-messages.js'; import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js'; import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js'; import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js'; @@ -212,17 +213,11 @@ import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; import * as ep___i_unpin from './endpoints/i/unpin.js'; import * as ep___i_updateEmail from './endpoints/i/update-email.js'; import * as ep___i_update from './endpoints/i/update.js'; -import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js'; import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; -import * as ep___messaging_history from './endpoints/messaging/history.js'; -import * as ep___messaging_messages from './endpoints/messaging/messages.js'; -import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js'; -import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; -import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; @@ -283,6 +278,9 @@ import * as ep___flash_myLikes from './endpoints/flash/my-likes.js'; import * as ep___ping from './endpoints/ping.js'; import * as ep___pinnedUsers from './endpoints/pinned-users.js'; import * as ep___promo_read from './endpoints/promo/read.js'; +import * as ep___roles_list from './endpoints/roles/list.js'; +import * as ep___roles_show from './endpoints/roles/show.js'; +import * as ep___roles_users from './endpoints/roles/users.js'; import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; import * as ep___resetDb from './endpoints/reset-db.js'; import * as ep___resetPassword from './endpoints/reset-password.js'; @@ -300,18 +298,6 @@ import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; -import * as ep___users_groups_create from './endpoints/users/groups/create.js'; -import * as ep___users_groups_delete from './endpoints/users/groups/delete.js'; -import * as ep___users_groups_invitations_accept from './endpoints/users/groups/invitations/accept.js'; -import * as ep___users_groups_invitations_reject from './endpoints/users/groups/invitations/reject.js'; -import * as ep___users_groups_invite from './endpoints/users/groups/invite.js'; -import * as ep___users_groups_joined from './endpoints/users/groups/joined.js'; -import * as ep___users_groups_leave from './endpoints/users/groups/leave.js'; -import * as ep___users_groups_owned from './endpoints/users/groups/owned.js'; -import * as ep___users_groups_pull from './endpoints/users/groups/pull.js'; -import * as ep___users_groups_show from './endpoints/users/groups/show.js'; -import * as ep___users_groups_transfer from './endpoints/users/groups/transfer.js'; -import * as ep___users_groups_update from './endpoints/users/groups/update.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js'; @@ -401,6 +387,7 @@ const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useCla const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default }; const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default }; const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default }; +const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default }; const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default }; const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default }; const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default }; @@ -505,6 +492,7 @@ const $i_2fa_keyDone: Provider = { provide: 'ep:i/2fa/key-done', useClass: ep___ const $i_2fa_passwordLess: Provider = { provide: 'ep:i/2fa/password-less', useClass: ep___i_2fa_passwordLess.default }; const $i_2fa_registerKey: Provider = { provide: 'ep:i/2fa/register-key', useClass: ep___i_2fa_registerKey.default }; const $i_2fa_register: Provider = { provide: 'ep:i/2fa/register', useClass: ep___i_2fa_register.default }; +const $i_2fa_updateKey: Provider = { provide: 'ep:i/2fa/update-key', useClass: ep___i_2fa_updateKey.default }; const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: ep___i_2fa_removeKey.default }; const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default }; const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default }; @@ -530,7 +518,6 @@ const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default }; const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default }; const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default }; -const $i_readAllMessagingMessages: Provider = { provide: 'ep:i/read-all-messaging-messages', useClass: ep___i_readAllMessagingMessages.default }; const $i_readAllUnreadNotes: Provider = { provide: 'ep:i/read-all-unread-notes', useClass: ep___i_readAllUnreadNotes.default }; const $i_readAnnouncement: Provider = { provide: 'ep:i/read-announcement', useClass: ep___i_readAnnouncement.default }; const $i_regenerateToken: Provider = { provide: 'ep:i/regenerate-token', useClass: ep___i_regenerateToken.default }; @@ -547,17 +534,11 @@ const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: e const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default }; const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default }; const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default }; -const $i_userGroupInvites: Provider = { provide: 'ep:i/user-group-invites', useClass: ep___i_userGroupInvites.default }; const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default }; const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default }; const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default }; const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default }; const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default }; -const $messaging_history: Provider = { provide: 'ep:messaging/history', useClass: ep___messaging_history.default }; -const $messaging_messages: Provider = { provide: 'ep:messaging/messages', useClass: ep___messaging_messages.default }; -const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/create', useClass: ep___messaging_messages_create.default }; -const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default }; -const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default }; const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; @@ -618,6 +599,9 @@ const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___ const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default }; const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default }; const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default }; +const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default }; +const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default }; +const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default }; const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default }; const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default }; const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default }; @@ -635,18 +619,6 @@ const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default }; const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default }; const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default }; -const $users_groups_create: Provider = { provide: 'ep:users/groups/create', useClass: ep___users_groups_create.default }; -const $users_groups_delete: Provider = { provide: 'ep:users/groups/delete', useClass: ep___users_groups_delete.default }; -const $users_groups_invitations_accept: Provider = { provide: 'ep:users/groups/invitations/accept', useClass: ep___users_groups_invitations_accept.default }; -const $users_groups_invitations_reject: Provider = { provide: 'ep:users/groups/invitations/reject', useClass: ep___users_groups_invitations_reject.default }; -const $users_groups_invite: Provider = { provide: 'ep:users/groups/invite', useClass: ep___users_groups_invite.default }; -const $users_groups_joined: Provider = { provide: 'ep:users/groups/joined', useClass: ep___users_groups_joined.default }; -const $users_groups_leave: Provider = { provide: 'ep:users/groups/leave', useClass: ep___users_groups_leave.default }; -const $users_groups_owned: Provider = { provide: 'ep:users/groups/owned', useClass: ep___users_groups_owned.default }; -const $users_groups_pull: Provider = { provide: 'ep:users/groups/pull', useClass: ep___users_groups_pull.default }; -const $users_groups_show: Provider = { provide: 'ep:users/groups/show', useClass: ep___users_groups_show.default }; -const $users_groups_transfer: Provider = { provide: 'ep:users/groups/transfer', useClass: ep___users_groups_transfer.default }; -const $users_groups_update: Provider = { provide: 'ep:users/groups/update', useClass: ep___users_groups_update.default }; const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default }; const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default }; @@ -740,6 +712,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_roles_assign, $admin_roles_unassign, $admin_roles_updateDefaultPolicies, + $admin_roles_users, $announcements, $antennas_create, $antennas_delete, @@ -844,6 +817,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_2fa_passwordLess, $i_2fa_registerKey, $i_2fa_register, + $i_2fa_updateKey, $i_2fa_removeKey, $i_2fa_unregister, $i_apps, @@ -869,7 +843,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_pageLikes, $i_pages, $i_pin, - $i_readAllMessagingMessages, $i_readAllUnreadNotes, $i_readAnnouncement, $i_regenerateToken, @@ -886,17 +859,11 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_unpin, $i_updateEmail, $i_update, - $i_userGroupInvites, $i_webhooks_create, $i_webhooks_list, $i_webhooks_show, $i_webhooks_update, $i_webhooks_delete, - $messaging_history, - $messaging_messages, - $messaging_messages_create, - $messaging_messages_delete, - $messaging_messages_read, $meta, $emojis, $miauth_genToken, @@ -957,6 +924,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $ping, $pinnedUsers, $promo_read, + $roles_list, + $roles_show, + $roles_users, $requestResetPassword, $resetDb, $resetPassword, @@ -974,18 +944,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_following, $users_gallery_posts, $users_getFrequentlyRepliedUsers, - $users_groups_create, - $users_groups_delete, - $users_groups_invitations_accept, - $users_groups_invitations_reject, - $users_groups_invite, - $users_groups_joined, - $users_groups_leave, - $users_groups_owned, - $users_groups_pull, - $users_groups_show, - $users_groups_transfer, - $users_groups_update, $users_lists_create, $users_lists_delete, $users_lists_list, @@ -1073,6 +1031,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_roles_assign, $admin_roles_unassign, $admin_roles_updateDefaultPolicies, + $admin_roles_users, $announcements, $antennas_create, $antennas_delete, @@ -1177,6 +1136,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_2fa_passwordLess, $i_2fa_registerKey, $i_2fa_register, + $i_2fa_updateKey, $i_2fa_removeKey, $i_2fa_unregister, $i_apps, @@ -1202,7 +1162,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_pageLikes, $i_pages, $i_pin, - $i_readAllMessagingMessages, $i_readAllUnreadNotes, $i_readAnnouncement, $i_regenerateToken, @@ -1219,17 +1178,11 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_unpin, $i_updateEmail, $i_update, - $i_userGroupInvites, $i_webhooks_create, $i_webhooks_list, $i_webhooks_show, $i_webhooks_update, $i_webhooks_delete, - $messaging_history, - $messaging_messages, - $messaging_messages_create, - $messaging_messages_delete, - $messaging_messages_read, $meta, $emojis, $miauth_genToken, @@ -1290,6 +1243,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $ping, $pinnedUsers, $promo_read, + $roles_list, + $roles_show, + $roles_users, $requestResetPassword, $resetDb, $resetPassword, @@ -1305,18 +1261,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_following, $users_gallery_posts, $users_getFrequentlyRepliedUsers, - $users_groups_create, - $users_groups_delete, - $users_groups_invitations_accept, - $users_groups_invitations_reject, - $users_groups_invite, - $users_groups_joined, - $users_groups_leave, - $users_groups_owned, - $users_groups_pull, - $users_groups_show, - $users_groups_transfer, - $users_groups_update, $users_lists_create, $users_lists_delete, $users_lists_list, diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index c7f9916f97..c94884a78c 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, UsersRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { User } from '@/models/entities/User.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -45,7 +45,7 @@ export class GetterService { throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); } - return user; + return user as LocalUser | RemoteUser; } /** diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index d490097dea..bd3d8a28da 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -1,13 +1,13 @@ import { randomBytes } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; -import * as speakeasy from 'speakeasy'; +import * as OTPAuth from 'otpauth'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; -import type { ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import { bindThis } from '@/decorators.js'; @@ -105,7 +105,7 @@ export class SigninApiService { const user = await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull(), - }) as ILocalUser; + }) as LocalUser; if (user == null) { return error(404, { @@ -155,19 +155,19 @@ export class SigninApiService { }); } - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorSecret, - encoding: 'base32', - token: token, - window: 2, + const delta = OTPAuth.TOTP.validate({ + secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!), + digits: 6, + token, + window: 1, }); - if (verified) { - return this.signinService.signin(request, reply, user); - } else { + if (delta === null) { return await fail(403, { id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f', }); + } else { + return this.signinService.signin(request, reply, user); } } else if (body.credentialId && body.clientDataJSON && body.authenticatorData && body.signature) { if (!same && !profile.usePasswordLessLogin) { diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index c78d9f85cd..aaf1d10b42 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -1,9 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { SigninsRepository, UsersRepository } from '@/models/index.js'; +import type { SigninsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { IdService } from '@/core/IdService.js'; -import type { ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -25,7 +25,7 @@ export class SigninService { } @bindThis - public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser) { + public signin(request: FastifyRequest, reply: FastifyReply, user: LocalUser) { setImmediate(async () => { // Append signin history const record = await this.signinsRepository.insert({ diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index ffd7e203ea..41e8365d08 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -10,7 +10,7 @@ import { IdService } from '@/core/IdService.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; -import { ILocalUser } from '@/models/entities/User.js'; +import { LocalUser } from '@/models/entities/User.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { bindThis } from '@/decorators.js'; import { SigninService } from './SigninService.js'; @@ -194,7 +194,7 @@ export class SignupApiService { emailVerifyCode: null, }); - return this.signinService.signin(request, reply, account as ILocalUser); + return this.signinService.signin(request, reply, account as LocalUser); } catch (err) { throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); } diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index b27329b9a9..ed283eb834 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import Ajv from 'ajv'; import type { Schema, SchemaType } from '@/misc/schema.js'; -import type { CacheableLocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import { ApiError } from './error.js'; import type { IEndpointMeta } from './endpoints.js'; @@ -20,17 +20,17 @@ type File = { }; // TODO: paramsの型をT['params']のスキーマ定義から推論する -type executor = - (params: SchemaType, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record | null) => +type Executor = + (params: SchemaType, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record | null) => Promise>>; export abstract class Endpoint { - public exec: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; + public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; - constructor(meta: T, paramDef: Ps, cb: executor) { + constructor(meta: T, paramDef: Ps, cb: Executor) { const validate = ajv.compile(paramDef); - this.exec = (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { + this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { let cleanup: undefined | (() => void) = undefined; if (meta.requireFile) { diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 55e1900d51..4f521148e0 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -1,4 +1,5 @@ import type { Schema } from '@/misc/schema.js'; +import { RolePolicies } from '@/core/RoleService.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; @@ -65,6 +66,7 @@ import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; +import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -169,6 +171,7 @@ import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js'; import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js'; import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js'; import * as ep___i_2fa_register from './endpoints/i/2fa/register.js'; +import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js'; import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; import * as ep___i_apps from './endpoints/i/apps.js'; @@ -194,7 +197,6 @@ import * as ep___i_notifications from './endpoints/i/notifications.js'; import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; import * as ep___i_pages from './endpoints/i/pages.js'; import * as ep___i_pin from './endpoints/i/pin.js'; -import * as ep___i_readAllMessagingMessages from './endpoints/i/read-all-messaging-messages.js'; import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js'; import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js'; import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js'; @@ -211,17 +213,11 @@ import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; import * as ep___i_unpin from './endpoints/i/unpin.js'; import * as ep___i_updateEmail from './endpoints/i/update-email.js'; import * as ep___i_update from './endpoints/i/update.js'; -import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js'; import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; -import * as ep___messaging_history from './endpoints/messaging/history.js'; -import * as ep___messaging_messages from './endpoints/messaging/messages.js'; -import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js'; -import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; -import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; @@ -282,6 +278,9 @@ import * as ep___flash_myLikes from './endpoints/flash/my-likes.js'; import * as ep___ping from './endpoints/ping.js'; import * as ep___pinnedUsers from './endpoints/pinned-users.js'; import * as ep___promo_read from './endpoints/promo/read.js'; +import * as ep___roles_list from './endpoints/roles/list.js'; +import * as ep___roles_show from './endpoints/roles/show.js'; +import * as ep___roles_users from './endpoints/roles/users.js'; import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; import * as ep___resetDb from './endpoints/reset-db.js'; import * as ep___resetPassword from './endpoints/reset-password.js'; @@ -299,18 +298,6 @@ import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; -import * as ep___users_groups_create from './endpoints/users/groups/create.js'; -import * as ep___users_groups_delete from './endpoints/users/groups/delete.js'; -import * as ep___users_groups_invitations_accept from './endpoints/users/groups/invitations/accept.js'; -import * as ep___users_groups_invitations_reject from './endpoints/users/groups/invitations/reject.js'; -import * as ep___users_groups_invite from './endpoints/users/groups/invite.js'; -import * as ep___users_groups_joined from './endpoints/users/groups/joined.js'; -import * as ep___users_groups_leave from './endpoints/users/groups/leave.js'; -import * as ep___users_groups_owned from './endpoints/users/groups/owned.js'; -import * as ep___users_groups_pull from './endpoints/users/groups/pull.js'; -import * as ep___users_groups_show from './endpoints/users/groups/show.js'; -import * as ep___users_groups_transfer from './endpoints/users/groups/transfer.js'; -import * as ep___users_groups_update from './endpoints/users/groups/update.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js'; @@ -398,6 +385,7 @@ const eps = [ ['admin/roles/assign', ep___admin_roles_assign], ['admin/roles/unassign', ep___admin_roles_unassign], ['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies], + ['admin/roles/users', ep___admin_roles_users], ['announcements', ep___announcements], ['antennas/create', ep___antennas_create], ['antennas/delete', ep___antennas_delete], @@ -502,6 +490,7 @@ const eps = [ ['i/2fa/password-less', ep___i_2fa_passwordLess], ['i/2fa/register-key', ep___i_2fa_registerKey], ['i/2fa/register', ep___i_2fa_register], + ['i/2fa/update-key', ep___i_2fa_updateKey], ['i/2fa/remove-key', ep___i_2fa_removeKey], ['i/2fa/unregister', ep___i_2fa_unregister], ['i/apps', ep___i_apps], @@ -527,7 +516,6 @@ const eps = [ ['i/page-likes', ep___i_pageLikes], ['i/pages', ep___i_pages], ['i/pin', ep___i_pin], - ['i/read-all-messaging-messages', ep___i_readAllMessagingMessages], ['i/read-all-unread-notes', ep___i_readAllUnreadNotes], ['i/read-announcement', ep___i_readAnnouncement], ['i/regenerate-token', ep___i_regenerateToken], @@ -544,17 +532,11 @@ const eps = [ ['i/unpin', ep___i_unpin], ['i/update-email', ep___i_updateEmail], ['i/update', ep___i_update], - ['i/user-group-invites', ep___i_userGroupInvites], ['i/webhooks/create', ep___i_webhooks_create], ['i/webhooks/list', ep___i_webhooks_list], ['i/webhooks/show', ep___i_webhooks_show], ['i/webhooks/update', ep___i_webhooks_update], ['i/webhooks/delete', ep___i_webhooks_delete], - ['messaging/history', ep___messaging_history], - ['messaging/messages', ep___messaging_messages], - ['messaging/messages/create', ep___messaging_messages_create], - ['messaging/messages/delete', ep___messaging_messages_delete], - ['messaging/messages/read', ep___messaging_messages_read], ['meta', ep___meta], ['emojis', ep___emojis], ['miauth/gen-token', ep___miauth_genToken], @@ -615,6 +597,9 @@ const eps = [ ['ping', ep___ping], ['pinned-users', ep___pinnedUsers], ['promo/read', ep___promo_read], + ['roles/list', ep___roles_list], + ['roles/show', ep___roles_show], + ['roles/users', ep___roles_users], ['request-reset-password', ep___requestResetPassword], ['reset-db', ep___resetDb], ['reset-password', ep___resetPassword], @@ -632,18 +617,6 @@ const eps = [ ['users/following', ep___users_following], ['users/gallery/posts', ep___users_gallery_posts], ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers], - ['users/groups/create', ep___users_groups_create], - ['users/groups/delete', ep___users_groups_delete], - ['users/groups/invitations/accept', ep___users_groups_invitations_accept], - ['users/groups/invitations/reject', ep___users_groups_invitations_reject], - ['users/groups/invite', ep___users_groups_invite], - ['users/groups/joined', ep___users_groups_joined], - ['users/groups/leave', ep___users_groups_leave], - ['users/groups/owned', ep___users_groups_owned], - ['users/groups/pull', ep___users_groups_pull], - ['users/groups/show', ep___users_groups_show], - ['users/groups/transfer', ep___users_groups_transfer], - ['users/groups/update', ep___users_groups_update], ['users/lists/create', ep___users_lists_create], ['users/lists/delete', ep___users_lists_delete], ['users/lists/list', ep___users_lists_list], @@ -697,7 +670,7 @@ export interface IEndpointMeta { */ readonly requireAdmin?: boolean; - readonly requireRolePolicy?: string; + readonly requireRolePolicy?: keyof RolePolicies; /** * エンドポイントのリミテーションに関するやつ @@ -768,8 +741,8 @@ export interface IEndpoint { const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { return { name: name, - meta: ep.meta ?? {}, - params: ep.paramDef, + get meta() { return ep.meta ?? {}; }, + get params() { return ep.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 8fcbde591b..917242db3f 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -20,9 +20,10 @@ export const paramDef = { priority: { type: 'string' }, ratio: { type: 'integer' }, expiresAt: { type: 'integer' }, + startsAt: { type: 'integer' }, imageUrl: { type: 'string', minLength: 1 }, }, - required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'imageUrl'], + required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl'], } as const; // eslint-disable-next-line import/no-default-export @@ -39,6 +40,7 @@ export default class extends Endpoint { id: this.idService.genId(), createdAt: new Date(), expiresAt: new Date(ps.expiresAt), + startsAt: new Date(ps.startsAt), url: ps.url, imageUrl: ps.imageUrl, priority: ps.priority, 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 29e245ab95..0b6d006052 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -31,9 +31,7 @@ export default class extends Endpoint { private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId) - .andWhere('ad.expiresAt > :now', { now: new Date() }); - + const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId); const ads = await query.take(ps.limit).getMany(); return ads; 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 08e3c96ca9..dbab7e9d4f 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -30,8 +30,9 @@ export const paramDef = { priority: { type: 'string' }, ratio: { type: 'integer' }, expiresAt: { type: 'integer' }, + startsAt: { type: 'integer' }, }, - required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt'], + required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt'], } as const; // eslint-disable-next-line import/no-default-export @@ -54,6 +55,7 @@ export default class extends Endpoint { memo: ps.memo, imageUrl: ps.imageUrl, expiresAt: new Date(ps.expiresAt), + startsAt: new Date(ps.startsAt), }); }); } 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 2cc4e70e55..a8964af449 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 @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; 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 6376cb153c..1d27ac2137 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 @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -138,19 +138,13 @@ export const meta = { export const paramDef = { type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + url: { type: 'string' }, + }, anyOf: [ - { - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], - }, - { - properties: { - url: { type: 'string' }, - }, - required: ['url'], - }, + { required: ['fileId'] }, + { required: ['url'] }, ], } as const; @@ -161,6 +155,9 @@ export default class extends Endpoint { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { @@ -178,7 +175,12 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchFile); } - const isModerator = await this.roleService.isModerator(me); + const owner = file.userId ? await this.usersRepository.findOneByOrFail({ + id: file.userId, + }) : null; + + const iAmModerator = await this.roleService.isModerator(me); + const ownerIsModerator = owner ? await this.roleService.isModerator(owner) : false; return { id: file.id, @@ -207,8 +209,8 @@ export default class extends Endpoint { name: file.name, md5: file.md5, createdAt: file.createdAt.toISOString(), - requestIp: isModerator ? file.requestIp : null, - requestHeaders: isModerator ? file.requestHeaders : null, + requestIp: iAmModerator ? file.requestIp : null, + requestHeaders: iAmModerator && !ownerIsModerator ? file.requestHeaders : null, }; }); } 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 c683cd24c1..0cc60e9191 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 @@ -56,7 +56,7 @@ export default class extends Endpoint { await this.db.queryResultCache!.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packMany(ps.ids), + emojis: await this.emojiEntityService.packDetailedMany(ps.ids), }); }); } 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 1bb05c15c2..04c58050ff 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; import rndstr from 'rndstr'; -import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; @@ -17,7 +16,7 @@ export const meta = { errors: { noSuchFile: { message: 'No such file.', - code: 'MO_SUCH_FILE', + code: 'NO_SUCH_FILE', id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', }, }, 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 b4fc7fd6f5..8885a40fd9 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -92,7 +92,7 @@ export default class extends Endpoint { await this.db.queryResultCache!.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiAdded', { - emoji: await this.emojiEntityService.pack(copied.id), + emoji: await this.emojiEntityService.packDetailed(copied.id), }); return { 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 0c337237d3..f298baaedf 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 @@ -54,7 +54,7 @@ export default class extends Endpoint { } this.globalEventService.publishBroadcastStream('emojiDeleted', { - emojis: await this.emojiEntityService.packMany(emojis), + emojis: await this.emojiEntityService.packDetailedMany(emojis), }); }); } 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 c1a60a2773..a5fbe3f4ea 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -4,9 +4,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { ApiError } from '../../../error.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], @@ -57,7 +57,7 @@ export default class extends Endpoint { await this.db.queryResultCache!.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiDeleted', { - emojis: [ await this.emojiEntityService.pack(emoji) ], + emojis: [await this.emojiEntityService.packDetailed(emoji)], }); this.moderationLogService.insertModerationLog(me, 'deleteEmoji', { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts index b4a07324bb..e26f0506ce 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; 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 8e0ea2e117..df3c28deff 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 @@ -101,7 +101,7 @@ export default class extends Endpoint { .take(ps.limit) .getMany(); - return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false }); + return this.emojiEntityService.packDetailedMany(emojis); }); } } 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 1b1931f8e6..814668294f 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -98,7 +98,7 @@ export default class extends Endpoint { emojis = await q.take(ps.limit).getMany(); } - return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false }); + return this.emojiEntityService.packDetailedMany(emojis); }); } } 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 065965f64a..66547024f7 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 @@ -56,7 +56,7 @@ export default class extends Endpoint { await this.db.queryResultCache!.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packMany(ps.ids), + emojis: await this.emojiEntityService.packDetailedMany(ps.ids), }); }); } 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 51c0f329ac..c8992eeb04 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 @@ -52,7 +52,7 @@ export default class extends Endpoint { await this.db.queryResultCache!.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packMany(ps.ids), + emojis: await this.emojiEntityService.packDetailedMany(ps.ids), }); }); } 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 3329cab7b9..8a538c1003 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 @@ -54,7 +54,7 @@ export default class extends Endpoint { await this.db.queryResultCache!.remove(['meta_emojis']); this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: await this.emojiEntityService.packMany(ps.ids), + emojis: await this.emojiEntityService.packDetailedMany(ps.ids), }); }); } 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 22bedc7100..809bf77d6b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -3,9 +3,9 @@ import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['admin'], @@ -68,15 +68,15 @@ export default class extends Endpoint { await this.db.queryResultCache!.remove(['meta_emojis']); - const updated = await this.emojiEntityService.pack(emoji.id); + const updated = await this.emojiEntityService.packDetailed(emoji.id); if (emoji.name === ps.name) { this.globalEventService.publishBroadcastStream('emojiUpdated', { - emojis: [ updated ], + emojis: [updated], }); } else { this.globalEventService.publishBroadcastStream('emojiDeleted', { - emojis: [ await this.emojiEntityService.pack(emoji) ], + emojis: [await this.emojiEntityService.packDetailed(emoji)], }); this.globalEventService.publishBroadcastStream('emojiAdded', { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 2b19104ea7..9eef1b29c5 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -54,86 +54,22 @@ export const meta = { }, mascotImageUrl: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, default: '/assets/ai.png', }, bannerUrl: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, }, errorImageUrl: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, default: 'https://xn--931a.moe/aiart/yubitun.png', }, iconUrl: { type: 'string', optional: false, nullable: true, }, - maxNoteTextLength: { - type: 'number', - optional: false, nullable: false, - }, - emojis: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - aliases: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - category: { - type: 'string', - optional: false, nullable: true, - }, - host: { - type: 'string', - optional: false, nullable: true, - }, - url: { - type: 'string', - optional: false, nullable: false, - format: 'url', - }, - }, - }, - }, - ads: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - place: { - type: 'string', - optional: false, nullable: false, - }, - url: { - type: 'string', - optional: false, nullable: false, - format: 'url', - }, - imageUrl: { - type: 'string', - optional: false, nullable: false, - format: 'url', - }, - }, - }, - }, enableEmail: { type: 'boolean', optional: false, nullable: false, @@ -146,10 +82,6 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - proxyAccountName: { - type: 'string', - optional: false, nullable: true, - }, userStarForReactionFallback: { type: 'boolean', optional: true, nullable: false, @@ -228,7 +160,7 @@ export const meta = { optional: true, nullable: true, }, smtpPort: { - type: 'string', + type: 'number', optional: true, nullable: true, }, smtpUser: { @@ -299,6 +231,10 @@ export const meta = { type: 'boolean', optional: true, nullable: false, }, + policies: { + type: 'object', + optional: false, nullable: false, + }, }, }, } as const; @@ -349,7 +285,6 @@ export default class extends Endpoint { iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため defaultLightTheme: instance.defaultLightTheme, defaultDarkTheme: instance.defaultDarkTheme, enableEmail: instance.enableEmail, 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 9129f53f06..099e2ff220 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { QueueService } from '@/core/QueueService.js'; 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 32ad79918f..f12738bd3a 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -1,5 +1,5 @@ import { URL } from 'node:url'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; import { ApiError } from '../../../error.js'; 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 079b351add..910c90e78e 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; 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 9dc4105d14..5e26f61fa7 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; 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 cdaec13a3f..d0d52089e6 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 @@ -49,7 +49,7 @@ export default class extends Endpoint { const actor = await this.instanceActorService.getInstanceActor(); const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); - this.queueService.deliver(actor, this.apRendererService.renderActivity(this.apRendererService.renderFlag(actor, [targetUser.uri!], report.comment)), targetUser.inbox); + this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox); } await this.abuseUserReportsRepository.update(report.id, { 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 7bfb2f6625..b80aaba122 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -1,10 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import type { RolesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { RoleService } from '@/core/RoleService.js'; export const meta = { @@ -39,6 +37,10 @@ export const paramDef = { properties: { roleId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' }, + expiresAt: { + type: 'integer', + nullable: true, + }, }, required: [ 'roleId', @@ -56,12 +58,7 @@ export default class extends Endpoint { @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, - @Inject(DI.roleAssignmentsRepository) - private roleAssignmentsRepository: RoleAssignmentsRepository, - - private globalEventService: GlobalEventService, private roleService: RoleService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); @@ -78,19 +75,11 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchUser); } - const date = new Date(); - const created = await this.roleAssignmentsRepository.insert({ - id: this.idService.genId(), - createdAt: date, - roleId: role.id, - userId: user.id, - }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); + if (ps.expiresAt && ps.expiresAt <= Date.now()) { + return; + } - this.rolesRepository.update(ps.roleId, { - lastUsedAt: new Date(), - }); - - this.globalEventService.publishInternalEvent('userRoleAssigned', created); + await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null); }); } } 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 458a8d535b..edaf638ea9 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts @@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { RolesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; export const meta = { @@ -33,7 +32,7 @@ export default class extends Endpoint { const roles = await this.rolesRepository.find({ order: { lastUsedAt: 'DESC' }, }); - return await this.roleEntityService.packMany(roles, me, { detail: false }); + return await this.roleEntityService.packMany(roles, me); }); } } 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 c83f96191d..01028a086f 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts @@ -39,12 +39,12 @@ export default class extends Endpoint { private roleEntityService: RoleEntityService, ) { - super(meta, paramDef, async (ps) => { + super(meta, paramDef, async (ps, me) => { const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); if (role == null) { throw new ApiError(meta.errors.noSuchRole); } - return await this.roleEntityService.pack(role); + return await this.roleEntityService.pack(role, me); }); } } 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 141cc5ee89..45c4f76943 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -1,10 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import type { RolesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { RoleService } from '@/core/RoleService.js'; export const meta = { @@ -62,12 +60,7 @@ export default class extends Endpoint { @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, - @Inject(DI.roleAssignmentsRepository) - private roleAssignmentsRepository: RoleAssignmentsRepository, - - private globalEventService: GlobalEventService, private roleService: RoleService, - private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); @@ -84,18 +77,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchUser); } - const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: role.id }); - if (roleAssignment == null) { - throw new ApiError(meta.errors.notAssigned); - } - - await this.roleAssignmentsRepository.delete(roleAssignment.id); - - this.rolesRepository.update(ps.roleId, { - lastUsedAt: new Date(), - }); - - this.globalEventService.publishInternalEvent('userRoleUnassigned', roleAssignment); + await this.roleService.unassign(user.id, role.id); }); } } 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 6006816bcb..5a34eee96c 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 @@ -1,9 +1,6 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '@/server/api/error.js'; import { MetaService } from '@/core/MetaService.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts new file mode 100644 index 0000000000..35edca5460 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -0,0 +1,78 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin', 'role', 'users'], + + requireCredential: false, + requireAdmin: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '224eff5e-2488-4b18-b3e7-f50d94421648', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, + required: ['roleId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private queryService: QueryService, + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ + id: ps.roleId, + }); + + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) + .andWhere('assign.roleId = :roleId', { roleId: role.id }) + .andWhere(new Brackets(qb => { qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); + })) + .innerJoinAndSelect('assign.user', 'user'); + + const assigns = await query + .take(ps.limit) + .getMany(); + + return await Promise.all(assigns.map(async assign => ({ + id: assign.id, + createdAt: assign.createdAt, + user: await this.userEntityService.pack(assign.user!, me, { detail: true }), + expiresAt: assign.expiresAt, + }))); + }); + } +} 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 7434bf4c91..5ddc62f476 100644 --- a/packages/backend/src/server/api/endpoints/admin/send-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/send-email.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmailService } from '@/core/EmailService.js'; 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 823af6d8be..9d19efbbcf 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -59,12 +59,6 @@ export default class extends Endpoint { throw new Error('cannot show info of admin'); } - if (!await this.roleService.isAdministrator(_me)) { - return { - isSuspended: user.isSuspended, - }; - } - const signins = await this.signinsRepository.findBy({ userId: user.id }); const roles = await this.roleService.getUserRoles(user.id); @@ -89,7 +83,7 @@ export default class extends Endpoint { moderationNote: profile.moderationNote, signins, policies: await this.roleService.getUserPolicies(user.id), - roles: await this.roleEntityService.packMany(roles, me, { detail: false }), + roles: await this.roleEntityService.packMany(roles, me), }; }); } 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 354ef22aa7..a7531aae89 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -2,10 +2,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import type { Meta } from '@/models/entities/Meta.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { MetaService } from '@/core/MetaService.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index a1553b6a80..bc5d249ae5 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import type { UserListsRepository, UserGroupJoiningsRepository, AntennasRepository } from '@/models/index.js'; +import type { UserListsRepository, AntennasRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -22,12 +22,6 @@ export const meta = { id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f', }, - noSuchUserGroup: { - message: 'No such user group.', - code: 'NO_SUCH_USER_GROUP', - id: 'aa3c0b9a-8cae-47c0-92ac-202ce5906682', - }, - tooManyAntennas: { message: 'You cannot create antenna any more.', code: 'TOO_MANY_ANTENNAS', @@ -46,9 +40,8 @@ export const paramDef = { type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, - src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'group'] }, + src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, userListId: { type: 'string', format: 'misskey:id', nullable: true }, - userGroupId: { type: 'string', format: 'misskey:id', nullable: true }, keywords: { type: 'array', items: { type: 'array', items: { type: 'string', @@ -80,9 +73,6 @@ export default class extends Endpoint { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - private antennaEntityService: AntennaEntityService, private roleService: RoleService, private idService: IdService, @@ -97,7 +87,6 @@ export default class extends Endpoint { } let userList; - let userGroupJoining; if (ps.src === 'list' && ps.userListId) { userList = await this.userListsRepository.findOneBy({ @@ -108,15 +97,6 @@ export default class extends Endpoint { if (userList == null) { throw new ApiError(meta.errors.noSuchUserList); } - } else if (ps.src === 'group' && ps.userGroupId) { - userGroupJoining = await this.userGroupJoiningsRepository.findOneBy({ - userGroupId: ps.userGroupId, - userId: me.id, - }); - - if (userGroupJoining == null) { - throw new ApiError(meta.errors.noSuchUserGroup); - } } const antenna = await this.antennasRepository.insert({ @@ -126,7 +106,6 @@ export default class extends Endpoint { name: ps.name, src: ps.src, userListId: userList ? userList.id : null, - userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, keywords: ps.keywords, excludeKeywords: ps.excludeKeywords, users: ps.users, diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 1955eac949..3f85442131 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AntennasRepository, UserListsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; +import type { AntennasRepository, UserListsRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -25,12 +25,6 @@ export const meta = { code: 'NO_SUCH_USER_LIST', id: '1c6b35c9-943e-48c2-81e4-2844989407f7', }, - - noSuchUserGroup: { - message: 'No such user group.', - code: 'NO_SUCH_USER_GROUP', - id: '109ed789-b6eb-456e-b8a9-6059d567d385', - }, }, res: { @@ -45,9 +39,8 @@ export const paramDef = { properties: { antennaId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, - src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'group'] }, + src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, userListId: { type: 'string', format: 'misskey:id', nullable: true }, - userGroupId: { type: 'string', format: 'misskey:id', nullable: true }, keywords: { type: 'array', items: { type: 'array', items: { type: 'string', @@ -78,9 +71,6 @@ export default class extends Endpoint { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, private antennaEntityService: AntennaEntityService, private globalEventService: GlobalEventService, @@ -97,7 +87,6 @@ export default class extends Endpoint { } let userList; - let userGroupJoining; if (ps.src === 'list' && ps.userListId) { userList = await this.userListsRepository.findOneBy({ @@ -108,22 +97,12 @@ export default class extends Endpoint { if (userList == null) { throw new ApiError(meta.errors.noSuchUserList); } - } else if (ps.src === 'group' && ps.userGroupId) { - userGroupJoining = await this.userGroupJoiningsRepository.findOneBy({ - userGroupId: ps.userGroupId, - userId: me.id, - }); - - if (userGroupJoining == null) { - throw new ApiError(meta.errors.noSuchUserGroup); - } } await this.antennasRepository.update(antenna.id, { name: ps.name, src: ps.src, userListId: userList ? userList.id : null, - userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, keywords: ps.keywords, excludeKeywords: ps.excludeKeywords, users: ps.users, diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index 8bafb3b122..c45a86761c 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -1,8 +1,7 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; -import { ApiError } from '../../error.js'; export const meta = { tags: ['federation'], diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 9470dd3cbb..61e05531e6 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -3,7 +3,7 @@ import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, NotesRepository } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; -import type { CacheableLocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import { isActor, isPost, getApId } from '@/core/activitypub/type.js'; import type { SchemaType } from '@/misc/schema.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; @@ -114,7 +114,7 @@ export default class extends Endpoint { * URIからUserかNoteを解決する */ @bindThis - private async fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise | null> { + private async fetchAny(uri: string, me: LocalUser | null | undefined): Promise | null> { // ブロックしてたら中断 const fetchedMeta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null; @@ -147,7 +147,7 @@ export default class extends Endpoint { } @bindThis - private async mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { + private async mergePack(me: LocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { if (user != null) { return { type: 'User', diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 58f8835279..cdaa400137 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -82,6 +82,12 @@ export default class extends Endpoint { .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') .leftJoinAndSelect('note.channel', 'channel'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + } //#endregion const timeline = await query.take(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts index 862ef89268..2ab58e4309 100644 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts index 1d5b8f05f8..e40a53d82e 100644 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts index ec28fa75de..9a5aff4af9 100644 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/drive.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import DriveChart from '@/core/chart/charts/drive.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts index 6c24cbbb77..ed3a968681 100644 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ b/packages/backend/src/server/api/endpoints/charts/federation.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import FederationChart from '@/core/chart/charts/federation.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts index a6a538ea5c..c992d525c9 100644 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ b/packages/backend/src/server/api/endpoints/charts/instance.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import InstanceChart from '@/core/chart/charts/instance.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts index 8d03f2eaf1..5750cd5b78 100644 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/notes.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import NotesChart from '@/core/chart/charts/notes.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts index 87d56f38b7..5e372294b7 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index 7a61544aea..3f50918fa7 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { getJsonSchema } from '@/core/chart/core.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts index fdc385191f..0517b3283f 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/user/pv.ts b/packages/backend/src/server/api/endpoints/charts/user/pv.ts index 33652c3adf..8d1a9aee10 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/pv.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/pv.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts index f0f3e520da..f2ff413195 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts index d09f2512e5..1374f02046 100644 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ b/packages/backend/src/server/api/endpoints/charts/users.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import UsersChart from '@/core/chart/charts/users.js'; diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 55778c7ecb..5d88870ed2 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -1,72 +1,72 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; -import { GetterService } from '@/server/api/GetterService.js'; - -export const meta = { - tags: ['account', 'notes', 'clips'], - - requireCredential: true, - - kind: 'write:account', - - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', - }, - - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'aff017de-190e-434b-893e-33a9ff5049d8', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - clipId: { type: 'string', format: 'misskey:id' }, - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['clipId', 'noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, - - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - - private getterService: GetterService, - ) { - super(meta, paramDef, async (ps, me) => { - const clip = await this.clipsRepository.findOneBy({ - id: ps.clipId, - userId: me.id, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - 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; - }); - - await this.clipNotesRepository.delete({ - noteId: note.id, - clipId: clip.id, - }); - }); - } -} +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; + +export const meta = { + tags: ['account', 'notes', 'clips'], + + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', + }, + + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'aff017de-190e-434b-893e-33a9ff5049d8', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + clipId: { type: 'string', format: 'misskey:id' }, + noteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['clipId', 'noteId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + 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; + }); + + await this.clipNotesRepository.delete({ + noteId: note.id, + clipId: clip.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index e5bbfecbcf..a6ece0311b 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index e0a07a3640..271b33ef4b 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -39,19 +39,13 @@ export const meta = { export const paramDef = { type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + url: { type: 'string' }, + }, anyOf: [ - { - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], - }, - { - properties: { - url: { type: 'string' }, - }, - required: ['url'], - }, + { required: ['fileId'] }, + { required: ['url'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 0fe57de6a8..3141e0fc01 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index a17bca5aba..cfef793831 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -1,7 +1,6 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import type { DriveFilesRepository } from '@/models/index.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/email-address/available.ts b/packages/backend/src/server/api/endpoints/email-address/available.ts index 8a497a514e..0f13b14d01 100644 --- a/packages/backend/src/server/api/endpoints/email-address/available.ts +++ b/packages/backend/src/server/api/endpoints/email-address/available.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmailService } from '@/core/EmailService.js'; diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 77854afb33..325b758358 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -1,4 +1,4 @@ -import { IsNull, MoreThan } from 'typeorm'; +import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { EmojisRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -82,11 +82,7 @@ export default class extends Endpoint { }); return { - emojis: await this.emojiEntityService.packMany(emojis, { - omitId: true, - omitHost: true, - withUrl: true, - }), + emojis: await this.emojiEntityService.packSimpleMany(emojis), }; }); } diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index 13b91685a4..b38c97f60a 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import endpoints from '../endpoints.js'; diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index 91fc3ec98d..9e706db747 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import endpoints from '../endpoints.js'; diff --git a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts index ead6b037cc..6b6079ad51 100644 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts @@ -1,5 +1,5 @@ import ms from 'ms'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index e5d1df0018..60b24e9585 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -10,6 +10,8 @@ export const meta = { tags: ['federation'], requireCredential: false, + allowGet: true, + cacheSec: 3600, res: { type: 'array', 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 c19252f198..4596e0c0b5 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 @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { GetterService } from '@/server/api/GetterService.js'; diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index a652047d98..f21d9d5c33 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -1,13 +1,10 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository, FlashsRepository, PagesRepository } from '@/models/index.js'; +import type { FlashsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import { Page } from '@/models/entities/Page.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; -import { ApiError } from '../../error.js'; export const meta = { tags: ['flash'], diff --git a/packages/backend/src/server/api/endpoints/flash/show.ts b/packages/backend/src/server/api/endpoints/flash/show.ts index 48114c5a60..14720a8c8d 100644 --- a/packages/backend/src/server/api/endpoints/flash/show.ts +++ b/packages/backend/src/server/api/endpoints/flash/show.ts @@ -1,7 +1,5 @@ -import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, FlashsRepository } from '@/models/index.js'; -import type { Flash } from '@/models/entities/Flash.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts index 9ab17a61e8..cd4e413a40 100644 --- a/packages/backend/src/server/api/endpoints/flash/update.ts +++ b/packages/backend/src/server/api/endpoints/flash/update.ts @@ -1,5 +1,4 @@ import ms from 'ms'; -import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { FlashsRepository, DriveFilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts index dcb98485da..cca3e60614 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index f39c4e3767..7325e73cac 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GetterService } from '@/server/api/GetterService.js'; diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index ab5706e8ef..a8fdc44876 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 3d9d471502..cb8b6a2e3e 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -7,7 +7,6 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import { IdService } from '@/core/IdService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; export const meta = { tags: ['gallery'], diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index d261aaa966..f14d644a3a 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -2,11 +2,9 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/index.js'; -import { GalleryPost } from '@/models/entities/GalleryPost.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; export const meta = { tags: ['gallery'], diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index ec9ac1ef90..6c31075e05 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -1,7 +1,10 @@ -import * as speakeasy from 'speakeasy'; +import * as OTPAuth from 'otpauth'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserProfilesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -22,8 +25,14 @@ export const paramDef = { @Injectable() export default class extends Endpoint { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { const token = ps.token.replace(/\s/g, ''); @@ -34,13 +43,14 @@ export default class extends Endpoint { throw new Error('二段階認証の設定が開始されていません'); } - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorTempSecret, - encoding: 'base32', - token: token, + const delta = OTPAuth.TOTP.validate({ + secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret), + digits: 6, + token, + window: 1, }); - if (!verified) { + if (delta === null) { throw new Error('not verified'); } @@ -48,6 +58,12 @@ export default class extends Endpoint { twoFactorSecret: profile.twoFactorTempSecret, twoFactorEnabled: true, }); + + // Publish meUpdated event + this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { + detail: true, + includeSecrets: true, + })); }); } } 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 6e0849f2b2..ad33398da6 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 @@ -25,7 +25,7 @@ export const paramDef = { attestationObject: { type: 'string' }, password: { type: 'string' }, challengeId: { type: 'string' }, - name: { type: 'string' }, + name: { type: 'string', minLength: 1, maxLength: 30 }, }, required: ['clientDataJSON', 'attestationObject', 'password', 'challengeId', 'name'], } as const; diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts index 0655a86350..0ee9f556a8 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts @@ -1,12 +1,23 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, secure: true, + + errors: { + noKey: { + message: 'No security key.', + code: 'NO_SECURITY_KEY', + id: 'f9c54d7f-d4c2-4d3c-9a8g-a70daac86512', + }, + }, } as const; export const paramDef = { @@ -23,11 +34,45 @@ export default class extends Endpoint { constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { + if (ps.value === true) { + // セキュリティキーがなければパスワードレスを有効にはできない + const keyCount = await this.userSecurityKeysRepository.count({ + where: { + userId: me.id, + }, + select: { + id: true, + name: true, + lastUsed: true, + }, + }); + + if (keyCount === 0) { + await this.userProfilesRepository.update(me.id, { + usePasswordLessLogin: false, + }); + + throw new ApiError(meta.errors.noKey); + } + } + await this.userProfilesRepository.update(me.id, { usePasswordLessLogin: ps.value, }); + + // Publish meUpdated event + this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { + detail: true, + includeSecrets: true, + })); }); } } 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 a539c5c221..eb4d7f9c14 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -1,5 +1,5 @@ import bcrypt from 'bcryptjs'; -import * as speakeasy from 'speakeasy'; +import * as OTPAuth from 'otpauth'; import * as QRCode from 'qrcode'; import { Inject, Injectable } from '@nestjs/common'; import type { UserProfilesRepository } from '@/models/index.js'; @@ -42,25 +42,24 @@ export default class extends Endpoint { } // Generate user's secret key - const secret = speakeasy.generateSecret({ - length: 32, - }); + const secret = new OTPAuth.Secret(); await this.userProfilesRepository.update(me.id, { twoFactorTempSecret: secret.base32, }); // Get the data URL of the authenticator URL - const url = speakeasy.otpauthURL({ - secret: secret.base32, - encoding: 'base32', + const totp = new OTPAuth.TOTP({ + secret, + digits: 6, label: me.username, issuer: this.config.host, }); - const dataUrl = await QRCode.toDataURL(url); + const url = totp.toString(); + const qr = await QRCode.toDataURL(url); return { - qr: dataUrl, + qr, url, secret: secret.base32, label: me.username, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index f40ec9797d..4b726aed80 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -2,7 +2,6 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; -import type { UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -51,6 +50,24 @@ export default class extends Endpoint { id: ps.credentialId, }); + // 使われているキーがなくなったらパスワードレスログインをやめる + const keyCount = await this.userSecurityKeysRepository.count({ + where: { + userId: me.id, + }, + select: { + id: true, + name: true, + lastUsed: true, + }, + }); + + if (keyCount === 0) { + await this.userProfilesRepository.update(me.id, { + usePasswordLessLogin: false, + }); + } + // Publish meUpdated event this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { detail: true, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 4c5b151f78..e0e7ba6658 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,7 +1,9 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserProfilesRepository } from '@/models/index.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -24,6 +26,9 @@ export default class extends Endpoint { constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); @@ -38,7 +43,14 @@ export default class extends Endpoint { await this.userProfilesRepository.update(me.id, { twoFactorSecret: null, twoFactorEnabled: false, + usePasswordLessLogin: false, }); + + // Publish meUpdated event + this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { + detail: true, + includeSecrets: true, + })); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts new file mode 100644 index 0000000000..d98f60fa5f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts @@ -0,0 +1,78 @@ +import bcrypt from 'bcryptjs'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + requireCredential: true, + + secure: true, + + errors: { + noSuchKey: { + message: 'No such key.', + code: 'NO_SUCH_KEY', + id: 'f9c5467f-d492-4d3c-9a8g-a70dacc86512', + }, + + accessDenied: { + message: 'You do not have edit privilege of the channel.', + code: 'ACCESS_DENIED', + id: '1fb7cb09-d46a-4fff-b8df-057708cce513', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', minLength: 1, maxLength: 30 }, + credentialId: { type: 'string' }, + }, + required: ['name', 'credentialId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const key = await this.userSecurityKeysRepository.findOneBy({ + id: ps.credentialId, + }); + + if (key == null) { + throw new ApiError(meta.errors.noSuchKey); + } + + if (key.userId !== me.id) { + throw new ApiError(meta.errors.accessDenied); + } + + await this.userSecurityKeysRepository.update(key.id, { + name: ps.name, + }); + + // Publish meUpdated event + this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { + detail: true, + includeSecrets: true, + })); + + return {}; + }); + } +} 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 d7109c6953..102dae4fb7 100644 --- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -1,6 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/i/export-blocking.ts b/packages/backend/src/server/api/endpoints/i/export-blocking.ts index 770708e685..4be88cbc2b 100644 --- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/export-blocking.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/export-favorites.ts b/packages/backend/src/server/api/endpoints/i/export-favorites.ts index b32f39d3e5..f522d4c409 100644 --- a/packages/backend/src/server/api/endpoints/i/export-favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/export-favorites.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/export-following.ts b/packages/backend/src/server/api/endpoints/i/export-following.ts index fcaa59b12d..1741781c0f 100644 --- a/packages/backend/src/server/api/endpoints/i/export-following.ts +++ b/packages/backend/src/server/api/endpoints/i/export-following.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/export-mute.ts b/packages/backend/src/server/api/endpoints/i/export-mute.ts index 37bef0a117..8e8042b1f9 100644 --- a/packages/backend/src/server/api/endpoints/i/export-mute.ts +++ b/packages/backend/src/server/api/endpoints/i/export-mute.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/export-notes.ts b/packages/backend/src/server/api/endpoints/i/export-notes.ts index 9d2505e403..ed54c9991c 100644 --- a/packages/backend/src/server/api/endpoints/i/export-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/export-notes.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts index 0f8e4bca76..5c2be38b71 100644 --- a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 706e0d2089..e3897d38bd 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,7 +1,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js'; -import { notificationTypes } from '@/types.js'; +import { obsoleteNotificationTypes, notificationTypes } from '@/types.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteReadService } from '@/core/NoteReadService.js'; @@ -41,11 +41,12 @@ export const paramDef = { following: { type: 'boolean', default: false }, unreadOnly: { type: 'boolean', default: false }, markAsRead: { type: 'boolean', default: true }, + // 後方互換のため、廃止された通知タイプも受け付ける includeTypes: { type: 'array', items: { - type: 'string', enum: notificationTypes, + type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], } }, excludeTypes: { type: 'array', items: { - type: 'string', enum: notificationTypes, + type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], } }, }, required: [], @@ -84,6 +85,10 @@ export default class extends Endpoint { if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { return []; } + + const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; + const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; + const followingQuery = this.followingsRepository.createQueryBuilder('following') .select('following.followeeId') .where('following.followerId = :followerId', { followerId: me.id }); @@ -143,10 +148,10 @@ export default class extends Endpoint { query.setParameters(followingQuery.getParameters()); } - if (ps.includeTypes && ps.includeTypes.length > 0) { - query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes }); - } else if (ps.excludeTypes && ps.excludeTypes.length > 0) { - query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes }); + if (includeTypes && includeTypes.length > 0) { + query.andWhere('notification.type IN (:...includeTypes)', { includeTypes }); + } else if (excludeTypes && excludeTypes.length > 0) { + query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes }); } if (ps.unreadOnly) { diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts index f31b0dc35e..d4af00027e 100644 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ b/packages/backend/src/server/api/endpoints/i/pin.ts @@ -1,6 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NotePiningService } from '@/core/NotePiningService.js'; import { ApiError } from '../../error.js'; diff --git a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts deleted file mode 100644 index 109d6d1068..0000000000 --- a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['account', 'messaging'], - - requireCredential: true, - - kind: 'write:account', -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps, me) => { - // Update documents - await this.messagingMessagesRepository.update({ - recipientId: me.id, - isRead: false, - }, { - isRead: true, - }); - - const joinings = await this.userGroupJoiningsRepository.findBy({ userId: me.id }); - - await Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder().update() - .set({ - reads: (() => `array_append("reads", '${me.id}')`) as any, - }) - .where('groupId = :groupId', { groupId: j.userGroupId }) - .andWhere('userId != :userId', { userId: me.id }) - .andWhere('NOT (:userId = ANY(reads))', { userId: me.id }) - .execute())); - - this.globalEventService.publishMainStream(me.id, 'readAllMessagingMessages'); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index cb5b4b0a60..b8922b91e5 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; -import type { UsersRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts index 9a735e1168..db239dc284 100644 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ b/packages/backend/src/server/api/endpoints/i/unpin.ts @@ -1,6 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NotePiningService } from '@/core/NotePiningService.js'; import { ApiError } from '../../error.js'; 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 b656c5c51d..4f543a6472 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -73,8 +73,8 @@ export default class extends Endpoint { } if (ps.email != null) { - const available = await this.emailService.validateEmailForAccount(ps.email); - if (!available) { + const res = await this.emailService.validateEmailForAccount(ps.email); + if (!res.available) { throw new ApiError(meta.errors.unavailable); } } diff --git a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts deleted file mode 100644 index 1ad2f7d68f..0000000000 --- a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserGroupInvitationsRepository } from '@/models/index.js'; -import { QueryService } from '@/core/QueryService.js'; -import { UserGroupInvitationEntityService } from '@/core/entities/UserGroupInvitationEntityService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['account', 'groups'], - - requireCredential: true, - - kind: 'read:user-groups', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - group: { - type: 'object', - optional: false, nullable: false, - ref: 'UserGroup', - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupInvitationsRepository) - private userGroupInvitationsRepository: UserGroupInvitationsRepository, - - private userGroupInvitationEntityService: UserGroupInvitationEntityService, - private queryService: QueryService, - ) { - super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.userGroupInvitationsRepository.createQueryBuilder('invitation'), ps.sinceId, ps.untilId) - .andWhere('invitation.userId = :meId', { meId: me.id }) - .leftJoinAndSelect('invitation.userGroup', 'user_group'); - - const invitations = await query - .take(ps.limit) - .getMany(); - - return await this.userGroupInvitationEntityService.packMany(invitations); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/messaging/history.ts b/packages/backend/src/server/api/endpoints/messaging/history.ts deleted file mode 100644 index 0b6099d4ac..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/history.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Brackets } from 'typeorm'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; -import type { MutingsRepository, UserGroupJoiningsRepository, MessagingMessagesRepository } from '@/models/index.js'; -import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true, - - kind: 'read:messaging', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'MessagingMessage', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - group: { type: 'boolean', default: false }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private messagingMessageEntityService: MessagingMessageEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const mute = await this.mutingsRepository.findBy({ - muterId: me.id, - }); - - const groups = ps.group ? await this.userGroupJoiningsRepository.findBy({ - userId: me.id, - }).then(xs => xs.map(x => x.userGroupId)) : []; - - if (ps.group && groups.length === 0) { - return []; - } - - const history: MessagingMessage[] = []; - - for (let i = 0; i < ps.limit; i++) { - const found = ps.group - ? history.map(m => m.groupId!) - : history.map(m => (m.userId === me.id) ? m.recipientId! : m.userId!); - - const query = this.messagingMessagesRepository.createQueryBuilder('message') - .orderBy('message.createdAt', 'DESC'); - - if (ps.group) { - query.where('message.groupId IN (:...groups)', { groups: groups }); - - if (found.length > 0) { - query.andWhere('message.groupId NOT IN (:...found)', { found: found }); - } - } else { - query.where(new Brackets(qb => { qb - .where('message.userId = :userId', { userId: me.id }) - .orWhere('message.recipientId = :userId', { userId: me.id }); - })); - query.andWhere('message.groupId IS NULL'); - - if (found.length > 0) { - query.andWhere('message.userId NOT IN (:...found)', { found: found }); - query.andWhere('message.recipientId NOT IN (:...found)', { found: found }); - } - - if (mute.length > 0) { - query.andWhere('message.userId NOT IN (:...mute)', { mute: mute.map(m => m.muteeId) }); - query.andWhere('message.recipientId NOT IN (:...mute)', { mute: mute.map(m => m.muteeId) }); - } - } - - const message = await query.getOne(); - - if (message) { - history.push(message); - } else { - break; - } - } - - return await Promise.all(history.map(h => this.messagingMessageEntityService.pack(h.id, me))); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts deleted file mode 100644 index 3673e252ae..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Brackets } from 'typeorm'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, UserGroupsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { QueryService } from '@/core/QueryService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js'; -import { MessagingService } from '@/core/MessagingService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; -import { GetterService } from '@/server/api/GetterService.js'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true, - - kind: 'read:messaging', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'MessagingMessage', - }, - }, - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '11795c64-40ea-4198-b06e-3c873ed9039d', - }, - - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f', - }, - - groupAccessDenied: { - message: 'You can not read messages of groups that you have not joined.', - code: 'GROUP_ACCESS_DENIED', - id: 'a053a8dd-a491-4718-8f87-50775aad9284', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - markAsRead: { type: 'boolean', default: true }, - }, - anyOf: [ - { - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId'], - }, - ], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - - @Inject(DI.userGroupsRepository) - private userGroupRepository: UserGroupsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private messagingMessageEntityService: MessagingMessageEntityService, - private messagingService: MessagingService, - private userEntityService: UserEntityService, - private queryService: QueryService, - private getterService: GetterService, - ) { - super(meta, paramDef, async (ps, me) => { - if (ps.userId != null) { - // Fetch recipient (user) - const recipient = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - const query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId) - .andWhere(new Brackets(qb => { qb - .where(new Brackets(qb => { qb - .where('message.userId = :meId') - .andWhere('message.recipientId = :recipientId'); - })) - .orWhere(new Brackets(qb => { qb - .where('message.userId = :recipientId') - .andWhere('message.recipientId = :meId'); - })); - })) - .setParameter('meId', me.id) - .setParameter('recipientId', recipient.id); - - const messages = await query.take(ps.limit).getMany(); - - // Mark all as read - if (ps.markAsRead) { - this.messagingService.readUserMessagingMessage(me.id, recipient.id, messages.filter(m => m.recipientId === me.id).map(x => x.id)); - - // リモートユーザーとのメッセージだったら既読配信 - if (this.userEntityService.isLocalUser(me) && this.userEntityService.isRemoteUser(recipient)) { - this.messagingService.deliverReadActivity(me, recipient, messages); - } - } - - return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { - populateRecipient: false, - }))); - } else if (ps.groupId != null) { - // Fetch recipient (group) - const recipientGroup = await this.userGroupRepository.findOneBy({ id: ps.groupId }); - - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // check joined - const joining = await this.userGroupJoiningsRepository.findOneBy({ - userId: me.id, - userGroupId: recipientGroup.id, - }); - - if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); - } - - const query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId) - .andWhere('message.groupId = :groupId', { groupId: recipientGroup.id }); - - const messages = await query.take(ps.limit).getMany(); - - // Mark all as read - if (ps.markAsRead) { - this.messagingService.readGroupMessagingMessage(me.id, recipientGroup.id, messages.map(x => x.id)); - } - - return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, { - populateGroup: false, - }))); - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts deleted file mode 100644 index e9ffc7a9eb..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; -import type { UserGroup } from '@/models/entities/UserGroup.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { MessagingService } from '@/core/MessagingService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true, - - kind: 'write:messaging', - - limit: { - duration: ms('1hour'), - max: 120, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'MessagingMessage', - }, - - errors: { - recipientIsYourself: { - message: 'You can not send a message to yourself.', - code: 'RECIPIENT_IS_YOURSELF', - id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '11795c64-40ea-4198-b06e-3c873ed9039d', - }, - - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537', - }, - - groupAccessDenied: { - message: 'You can not send messages to groups that you have not joined.', - code: 'GROUP_ACCESS_DENIED', - id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd', - }, - - noSuchFile: { - message: 'No such file.', - code: 'NO_SUCH_FILE', - id: '4372b8e2-185d-4146-8749-2f68864a3e5f', - }, - - contentRequired: { - message: 'Content required. You need to set text or fileId.', - code: 'CONTENT_REQUIRED', - id: '25587321-b0e6-449c-9239-f8925092942c', - }, - - youHaveBeenBlocked: { - message: 'You cannot send a message because you have been blocked by this user.', - code: 'YOU_HAVE_BEEN_BLOCKED', - id: 'c15a5199-7422-4968-941a-2a462c478f7d', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - text: { type: 'string', nullable: true, maxLength: 3000 }, - fileId: { type: 'string', format: 'misskey:id' }, - }, - anyOf: [ - { - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId'], - }, - ], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - - private getterService: GetterService, - private messagingService: MessagingService, - ) { - super(meta, paramDef, async (ps, me) => { - let recipientUser: User | null; - let recipientGroup: UserGroup | null; - - if (ps.userId != null) { - // Myself - if (ps.userId === me.id) { - throw new ApiError(meta.errors.recipientIsYourself); - } - - // Fetch recipient (user) - recipientUser = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - // Check blocking - const block = await this.blockingsRepository.findOneBy({ - blockerId: recipientUser.id, - blockeeId: me.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } else if (ps.groupId != null) { - // Fetch recipient (group) - recipientGroup = await this.userGroupsRepository.findOneBy({ id: ps.groupId! }); - - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // check joined - const joining = await this.userGroupJoiningsRepository.findOneBy({ - userId: me.id, - userGroupId: recipientGroup.id, - }); - - if (joining == null) { - throw new ApiError(meta.errors.groupAccessDenied); - } - } - - let file = null; - if (ps.fileId != null) { - file = await this.driveFilesRepository.findOneBy({ - id: ps.fileId, - userId: me.id, - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - // テキストが無いかつ添付ファイルも無かったらエラー - if (ps.text == null && file == null) { - throw new ApiError(meta.errors.contentRequired); - } - - return await this.messagingService.createMessage(me, recipientUser, recipientGroup, ps.text, file); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts deleted file mode 100644 index cd74f5f197..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MessagingMessagesRepository } from '@/models/index.js'; -import { MessagingService } from '@/core/MessagingService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true, - - kind: 'write:messaging', - - limit: { - duration: ms('1hour'), - max: 300, - minInterval: ms('1sec'), - }, - - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: '54b5b326-7925-42cf-8019-130fda8b56af', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - messageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['messageId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - - private messagingService: MessagingService, - ) { - super(meta, paramDef, async (ps, me) => { - const message = await this.messagingMessagesRepository.findOneBy({ - id: ps.messageId, - userId: me.id, - }); - - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } - - await this.messagingService.deleteMessage(message); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts deleted file mode 100644 index bddb6d932d..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MessagingMessagesRepository } from '@/models/index.js'; -import { MessagingService } from '@/core/MessagingService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true, - - kind: 'write:messaging', - - errors: { - noSuchMessage: { - message: 'No such message.', - code: 'NO_SUCH_MESSAGE', - id: '86d56a2f-a9c3-4afb-b13c-3e9bfef9aa14', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - messageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['messageId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - - private messagingService: MessagingService, - ) { - super(meta, paramDef, async (ps, me) => { - const message = await this.messagingMessagesRepository.findOneBy({ id: ps.messageId }); - - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } - - if (message.recipientId) { - await this.messagingService.readUserMessagingMessage(me.id, message.userId, [message.id]).catch(err => { - if (err.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage); - throw err; - }); - } else if (message.groupId) { - await this.messagingService.readGroupMessagingMessage(me.id, message.groupId, [message.id]).catch(err => { - if (err.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage); - throw err; - }); - } - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 2fa7a09d49..cdb314a873 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,7 +1,7 @@ -import { IsNull, MoreThan } from 'typeorm'; +import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { AdsRepository, EmojisRepository, UsersRepository } from '@/models/index.js'; -import { MAX_NOTE_TEXT_LENGTH, DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import type { AdsRepository, UsersRepository } from '@/models/index.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'; @@ -262,6 +262,7 @@ export default class extends Endpoint { const ads = await this.adsRepository.find({ where: { expiresAt: MoreThan(new Date()), + startsAt: LessThanOrEqual(new Date()), }, }); @@ -294,7 +295,7 @@ export default class extends Endpoint { iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため + maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, defaultLightTheme: instance.defaultLightTheme, defaultDarkTheme: instance.defaultDarkTheme, ads: ads.map(ad => ({ diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts new file mode 100644 index 0000000000..6bff7fc0c9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -0,0 +1,263 @@ +process.env.NODE_ENV = 'test'; + +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import { describe, test, expect } from '@jest/globals'; +import { getValidator } from '../../../../../test/prelude/get-api-validator.js'; +import { paramDef } from './create.js'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + +const VALID = true; +const INVALID = false; + +describe('api:notes/create', () => { + describe('validation', () => { + const v = getValidator(paramDef); + const tooLong = readFile(_dirname + '/../../../../../test/resources/misskey.svg', 'utf-8'); + + test('reject empty', () => { + const valid = v({ }); + expect(valid).toBe(INVALID); + }); + + describe('text', () => { + test('simple post', () => { + expect(v({ text: 'Hello, world!' })) + .toBe(VALID); + }); + + test('null post', () => { + expect(v({ text: null })) + .toBe(INVALID); + }); + + test('0 characters post', () => { + expect(v({ text: '' })) + .toBe(INVALID); + }); + + test('over 3000 characters post', async () => { + expect(v({ text: await tooLong })) + .toBe(INVALID); + }); + }); + + describe('cw', () => { + test('simple cw', () => { + expect(v({ text: 'Hello, world!', cw: 'Hello, world!' })) + .toBe(VALID); + }); + + test('null cw', () => { + expect(v({ text: 'Body', cw: null })) + .toBe(VALID); + }); + + test('0 characters cw', () => { + expect(v({ text: 'Body', cw: '' })) + .toBe(VALID); + }); + + test('reject only cw', () => { + expect(v({ cw: 'Hello, world!' })) + .toBe(INVALID); + }); + + test('over 100 characters cw', async () => { + expect(v({ text: 'Body', cw: await tooLong })) + .toBe(INVALID); + }); + }); + + describe('visibility', () => { + test('public', () => { + expect(v({ text: 'Hello, world!', visibility: 'public' })) + .toBe(VALID); + }); + + test('home', () => { + expect(v({ text: 'Hello, world!', visibility: 'home' })) + .toBe(VALID); + }); + + test('followers', () => { + expect(v({ text: 'Hello, world!', visibility: 'followers' })) + .toBe(VALID); + }); + + test('reject only visibility', () => { + expect(v({ visibility: 'public' })) + .toBe(INVALID); + }); + + test('reject invalid visibility', () => { + expect(v({ text: 'Hello, world!', visibility: 'invalid' })) + .toBe(INVALID); + }); + + test('reject null visibility', () => { + expect(v({ text: 'Hello, world!', visibility: null })) + .toBe(INVALID); + }); + + describe('visibility:specified', () => { + test('specified without visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified' })) + .toBe(VALID); + }); + + test('specified with empty visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: [] })) + .toBe(VALID); + }); + + test('reject specified with non unique visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: ['1', '1', '2'] })) + .toBe(INVALID); + }); + + test('reject specified with null visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: null })) + .toBe(INVALID); + }); + }); + }); + + describe('fileIds', () => { + test('only fileIds', () => { + expect(v({ fileIds: ['1', '2', '3'] })) + .toBe(VALID); + }); + + test('text and fileIds', () => { + expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'] })) + .toBe(VALID); + }); + + test('reject null fileIds', () => { + expect(v({ fileIds: null })) + .toBe(INVALID); + }); + + test('reject text and null fileIds (複合的なanyOfのバリデーションが正しく動作する)', () => { + expect(v({ text: 'Hello, world!', fileIds: null })) + .toBe(INVALID); + }); + + test('reject 0 files', () => { + expect(v({ fileIds: [] })) + .toBe(INVALID); + }); + + test('reject non unique', () => { + expect(v({ fileIds: ['1', '1', '2'] })) + .toBe(INVALID); + }); + + test('reject invalid id', () => { + expect(v({ fileIds: ['あ'] })) + .toBe(INVALID); + }); + + test('reject over 17 files', () => { + const valid = v({ text: 'Hello, world!', fileIds: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18'] }); + expect(valid).toBe(INVALID); + }); + }); + + describe('poll', () => { + test('note with poll', () => { + expect(v({ text: 'Hello, world!', poll: { choices: ['a', 'b', 'c'] } })) + .toBe(VALID); + }); + + test('null poll', () => { + expect(v({ text: 'Hello, world!', poll: null })) + .toBe(VALID); + }); + + test('allow only poll', () => { + expect(v({ poll: { choices: ['a', 'b', 'c'] } })) + .toBe(VALID); + }); + + test('poll with expiresAt', async () => { + expect(v({ poll: { choices: ['a', 'b', 'c'], expiresAt: 1 } })) + .toBe(VALID); + }); + + test('poll with expiredAfter', async () => { + expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 1 } })) + .toBe(VALID); + }); + + test('reject poll without choices', () => { + expect(v({ poll: { } })) + .toBe(INVALID); + }); + + test('reject poll with empty choices', () => { + expect(v({ poll: { choices: [] } })) + .toBe(INVALID); + }); + + test('reject poll with null choices', () => { + expect(v({ poll: { choices: null } })) + .toBe(INVALID); + }); + + test('reject poll with 1 choice', () => { + expect(v({ poll: { choices: ['a'] } })) + .toBe(INVALID); + }); + + test('reject poll with too long choice', async () => { + expect(v({ poll: { choices: [await tooLong, '2'] } })) + .toBe(INVALID); + }); + + test('reject poll with too many choices', () => { + expect(v({ poll: { choices: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] } })) + .toBe(INVALID); + }); + + test('reject poll with non unique choices', () => { + expect(v({ poll: { choices: ['a', 'a', 'b', 'c'] } })) + .toBe(INVALID); + }); + + test('reject poll with expiredAfter 0', async () => { + expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 0 } })) + .toBe(INVALID); + }); + }); + + describe('renote', () => { + test('just a renote', () => { + expect(v({ renoteId: '1' })) + .toBe(VALID); + }); + test('just a quote', () => { + expect(v({ text: 'Hello, world!', renoteId: '1' })) + .toBe(VALID); + }); + test('reject invalid renoteId', () => { + expect(v({ renoteId: 'あ' })) + .toBe(INVALID); + }); + }); + + test('text, fileIds and poll', () => { + expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'], poll: { choices: ['a', 'b', 'c'] } })) + .toBe(VALID); + }); + + test('text, invalid fileIds and invalid poll', () => { + expect(v({ text: 'Hello, world!', fileIds: ['あ'], poll: { choices: ['a'] } })) + .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 a709ab2f7a..786ad103b0 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -11,7 +11,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; -import { noteVisibilities } from '../../../../types.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -80,6 +79,12 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', }, + + noSuchFile: { + message: 'Some files are not found.', + code: 'NO_SUCH_FILE', + id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', + }, }, } as const; @@ -96,74 +101,56 @@ export const paramDef = { noExtractHashtags: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false }, replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true }, + + // anyOf内にバリデーションを書いても最初の一つしかチェックされない + // See https://github.com/misskey-dev/misskey/pull/10082 + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: false + }, + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + mediaIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + poll: { + type: 'object', + nullable: true, + properties: { + choices: { + type: 'array', + uniqueItems: true, + minItems: 2, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, + }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, + }, + required: ['choices'], + }, }, + // (re)note with text, files and poll are optional anyOf: [ - { - // (re)note with text, files and poll are optional - properties: { - text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, - }, - required: ['text'], - }, - { - // (re)note with files, text and poll are optional - properties: { - fileIds: { - type: 'array', - uniqueItems: true, - minItems: 1, - maxItems: 16, - items: { type: 'string', format: 'misskey:id' }, - }, - }, - required: ['fileIds'], - }, - { - // (re)note with files, text and poll are optional - properties: { - mediaIds: { - deprecated: true, - description: 'Use `fileIds` instead. If both are specified, this property is discarded.', - type: 'array', - uniqueItems: true, - minItems: 1, - maxItems: 16, - items: { type: 'string', format: 'misskey:id' }, - }, - }, - required: ['mediaIds'], - }, - { - // (re)note with poll, text and files are optional - properties: { - poll: { - type: 'object', - nullable: true, - properties: { - choices: { - type: 'array', - uniqueItems: true, - minItems: 2, - maxItems: 10, - items: { type: 'string', minLength: 1, maxLength: 50 }, - }, - multiple: { type: 'boolean' }, - expiresAt: { type: 'integer', nullable: true }, - expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, - }, - required: ['choices'], - }, - }, - required: ['poll'], - }, - { - // pure renote - properties: { - renoteId: { type: 'string', format: 'misskey:id', nullable: true }, - }, - required: ['renoteId'], - }, + { required: ['text'] }, + { required: ['renoteId'] }, + { required: ['fileIds'] }, + { required: ['mediaIds'] }, + { required: ['poll'] }, ], } as const; @@ -208,6 +195,10 @@ export default class extends Endpoint { .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') .setParameters({ fileIds }) .getMany(); + + if (files.length !== fileIds.length) { + throw new ApiError(meta.errors.noSuchFile); + } } let renote: Note | null = null; @@ -281,7 +272,7 @@ export default class extends Endpoint { files: files, poll: ps.poll ? { choices: ps.poll.choices, - multiple: ps.poll.multiple || false, + multiple: ps.poll.multiple ?? false, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, } : undefined, text: ps.text ?? undefined, diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 76834cfde9..cf939f6631 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 @@ export const meta = { tags: ['notes'], requireCredential: false, + allowGet: true, + cacheSec: 3600, res: { type: 'array', @@ -26,6 +28,7 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, + channelId: { type: 'string', nullable: true, format: 'misskey:id' }, }, required: [], } as const; @@ -41,7 +44,6 @@ export default class extends Endpoint { private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const max = 30; const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで const query = this.notesRepository.createQueryBuilder('note') @@ -62,12 +64,14 @@ export default class extends Endpoint { .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); + if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me); let notes = await query .orderBy('note.score', 'DESC') - .take(max) + .take(50) .getMany(); notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); 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 f396f7e584..18ed6d4e21 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,6 +1,6 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, UsersRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 92b82eb5de..dcb0d0adcb 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -4,7 +4,6 @@ import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index befaea4664..b9e06a7834 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,7 +1,6 @@ -import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; -import type { IRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -160,9 +159,9 @@ export default class extends Endpoint { // リモート投票の場合リプライ送信 if (note.userHost != null) { - const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser; + const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as RemoteUser; - this.queueService.deliver(me, this.apRendererService.renderActivity(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox); + this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox); } // リモートフォロワーにUpdate配信 diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 02ae212a30..f758bfe9b1 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -1,11 +1,9 @@ -import { DeepPartial } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { NoteReactionsRepository } from '@/models/index.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import type { FindOptionsWhere } from 'typeorm'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index 839f893db2..04e374d1ae 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ReactionService } from '@/core/ReactionService.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index cf90d7b5f6..207f0b4cf2 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,5 +1,5 @@ import ms from 'ms'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ReactionService } from '@/core/ReactionService.js'; 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 bcd793ac43..da1a4bcc46 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 @@ -36,32 +36,25 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + + tag: { type: 'string', minLength: 1 }, + query: { + type: 'array', + description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', + items: { + type: 'array', + items: { + type: 'string', + minLength: 1, + }, + minItems: 1, + }, + minItems: 1, + }, }, anyOf: [ - { - properties: { - tag: { type: 'string', minLength: 1 }, - }, - required: ['tag'], - }, - { - properties: { - query: { - type: 'array', - description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', - items: { - type: 'array', - items: { - type: 'string', - minLength: 1, - }, - minItems: 1, - }, - minItems: 1, - }, - }, - required: ['query'], - }, + { required: ['tag'] }, + { required: ['query'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 8eb031dfe3..ef47a3004d 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,4 +1,3 @@ -import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 145d3f5c83..e6de087c4a 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,7 +5,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -59,25 +58,15 @@ export default class extends Endpoint { private activeUsersChart: ActiveUsersChart, ) { super(meta, paramDef, async (ps, me) => { - const hasFollowing = (await this.followingsRepository.count({ - where: { - followerId: me.id, - }, - take: 1, - })) !== 0; + const followees = await this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }) + .getMany(); //#region Construct query - const followingQuery = this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30日前まで - .andWhere(new Brackets(qb => { qb - .where('note.userId = :meId', { meId: me.id }); - if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`); - })) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -88,8 +77,15 @@ export default class extends Endpoint { .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .setParameters(followingQuery.getParameters()); + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); + + if (followees.length > 0) { + const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; + + query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); + } else { + query.andWhere('note.userId = :meId', { meId: me.id }); + } this.queryService.generateChannelQuery(query, me); this.queryService.generateRepliesQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 3427a3eb5c..2e63eee263 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js'; diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts index cdf8d09f9e..6262c47fd0 100644 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/read.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NotificationService } from '@/core/NotificationService.js'; diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index 1841a84539..1d6fb567f0 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; import type { PagesRepository } from '@/models/index.js'; -import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index 651252afbb..bf2b2a431e 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -29,20 +29,14 @@ export const meta = { export const paramDef = { type: 'object', + properties: { + pageId: { type: 'string', format: 'misskey:id' }, + name: { type: 'string' }, + username: { type: 'string' }, + }, anyOf: [ - { - properties: { - pageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['pageId'], - }, - { - properties: { - name: { type: 'string' }, - username: { type: 'string' }, - }, - required: ['name', 'username'], - }, + { required: ['pageId'] }, + { required: ['name', 'username'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts index 4bb62b298e..5807bf101e 100644 --- a/packages/backend/src/server/api/endpoints/ping.ts +++ b/packages/backend/src/server/api/endpoints/ping.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 42b10a4fb3..3b6ebfe281 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -8,7 +8,6 @@ import { IdService } from '@/core/IdService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { EmailService } from '@/core/EmailService.js'; -import { ApiError } from '../error.js'; export const meta = { tags: ['reset password'], diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 526efbc2f6..655dd7cd83 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -4,7 +4,6 @@ import Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { resetDb } from '@/misc/reset-db.js'; -import { ApiError } from '../error.js'; export const meta = { tags: ['non-productive'], diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index cf7fcb7afd..e6f1af7b22 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,10 +1,8 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import type { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/index.js'; -import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../error.js'; export const meta = { tags: ['reset password'], diff --git a/packages/backend/src/server/api/endpoints/retention.ts b/packages/backend/src/server/api/endpoints/retention.ts index e3c2249cdd..e9c0fd4dcd 100644 --- a/packages/backend/src/server/api/endpoints/retention.ts +++ b/packages/backend/src/server/api/endpoints/retention.ts @@ -1,4 +1,3 @@ -import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { RetentionAggregationsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts new file mode 100644 index 0000000000..d61c6b8dc6 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; + +export const meta = { + tags: ['role'], + + requireCredential: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [ + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const roles = await this.rolesRepository.findBy({ + isPublic: true, + }); + return await this.roleEntityService.packMany(roles, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts new file mode 100644 index 0000000000..cc755dcc76 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/roles/show.ts @@ -0,0 +1,52 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { RolesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['role', 'users'], + + requireCredential: false, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: 'de5502bf-009a-4639-86c1-fec349e46dcb', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roleId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ + id: ps.roleId, + isPublic: true, + }); + + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + return await this.roleEntityService.pack(role, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts new file mode 100644 index 0000000000..607dc24206 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -0,0 +1,76 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['role', 'users'], + + requireCredential: false, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, + required: ['roleId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private queryService: QueryService, + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ + id: ps.roleId, + isPublic: true, + }); + + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) + .andWhere('assign.roleId = :roleId', { roleId: role.id }) + .andWhere(new Brackets(qb => { qb + .where('assign.expiresAt IS NULL') + .orWhere('assign.expiresAt > :now', { now: new Date() }); + })) + .innerJoinAndSelect('assign.user', 'user'); + + const assigns = await query + .take(ps.limit) + .getMany(); + + return await Promise.all(assigns.map(async assign => ({ + id: assign.id, + user: await this.userEntityService.pack(assign.user!, me, { detail: true }), + }))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index 8989a3073d..1620e8ae52 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -1,6 +1,6 @@ import * as os from 'node:os'; import si from 'systeminformation'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index 8bd0311dce..48a85758a0 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull } from 'typeorm'; import type { InstancesRepository, NoteReactionsRepository, NotesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 39ea1f2171..c88f7f2daf 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; export const meta = { diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 17ce920011..97f1310c36 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -46,25 +46,18 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + + userId: { type: 'string', format: 'misskey:id' }, + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, }, anyOf: [ - { - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - properties: { - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', - }, - }, - required: ['username', 'host'], - }, + { required: ['userId'] }, + { required: ['username', 'host'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 6dbda0d72f..d406594a2e 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -46,25 +46,18 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + + userId: { type: 'string', format: 'misskey:id' }, + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, }, anyOf: [ - { - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - properties: { - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', - }, - }, - required: ['username', 'host'], - }, + { required: ['userId'] }, + { required: ['username', 'host'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts deleted file mode 100644 index 24dbf5ca3c..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; -import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { IdService } from '@/core/IdService.js'; -import type { UserGroup } from '@/models/entities/UserGroup.js'; -import type { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['groups'], - - requireCredential: true, - - kind: 'write:user-groups', - - description: 'Create a new group.', - - limit: { - duration: ms('1hour'), - max: 10, - }, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'UserGroup', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - name: { type: 'string', minLength: 1, maxLength: 100 }, - }, - required: ['name'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private userGroupEntityService: UserGroupEntityService, - private idService: IdService, - ) { - super(meta, paramDef, async (ps, me) => { - const userGroup = await this.userGroupsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - userId: me.id, - name: ps.name, - } as UserGroup).then(x => this.userGroupsRepository.findOneByOrFail(x.identifiers[0])); - - // Push the owner - await this.userGroupJoiningsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - userId: me.id, - userGroupId: userGroup.id, - } as UserGroupJoining); - - return await this.userGroupEntityService.pack(userGroup); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts deleted file mode 100644 index d238ae9f16..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupsRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['groups'], - - requireCredential: true, - - kind: 'write:user-groups', - - description: 'Delete an existing group.', - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '63dbd64c-cd77-413f-8e08-61781e210b38', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - ) { - super(meta, paramDef, async (ps, me) => { - const userGroup = await this.userGroupsRepository.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - await this.userGroupsRepository.delete(userGroup.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts deleted file mode 100644 index f154a57f61..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupInvitationsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { IdService } from '@/core/IdService.js'; -import type { UserGroupJoining } from '@/models/entities/UserGroupJoining.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../../error.js'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true, - - kind: 'write:user-groups', - - description: 'Join a group the authenticated user has been invited to.', - - errors: { - noSuchInvitation: { - message: 'No such invitation.', - code: 'NO_SUCH_INVITATION', - id: '98c11eca-c890-4f42-9806-c8c8303ebb5e', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - invitationId: { type: 'string', format: 'misskey:id' }, - }, - required: ['invitationId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupInvitationsRepository) - private userGroupInvitationsRepository: UserGroupInvitationsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private idService: IdService, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the invitation - const invitation = await this.userGroupInvitationsRepository.findOneBy({ - id: ps.invitationId, - }); - - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - if (invitation.userId !== me.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - // Push the user - await this.userGroupJoiningsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - userId: me.id, - userGroupId: invitation.userGroupId, - } as UserGroupJoining); - - this.userGroupInvitationsRepository.delete(invitation.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts deleted file mode 100644 index 1fd3b2f4b3..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupInvitationsRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../../error.js'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true, - - kind: 'write:user-groups', - - description: 'Delete an existing group invitation for the authenticated user without joining the group.', - - errors: { - noSuchInvitation: { - message: 'No such invitation.', - code: 'NO_SUCH_INVITATION', - id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - invitationId: { type: 'string', format: 'misskey:id' }, - }, - required: ['invitationId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupInvitationsRepository) - private userGroupInvitationsRepository: UserGroupInvitationsRepository, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the invitation - const invitation = await this.userGroupInvitationsRepository.findOneBy({ - id: ps.invitationId, - }); - - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - if (invitation.userId !== me.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - await this.userGroupInvitationsRepository.delete(invitation.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts deleted file mode 100644 index 2e040c0601..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupsRepository, UserGroupJoiningsRepository, UserGroupInvitationsRepository } from '@/models/index.js'; -import { IdService } from '@/core/IdService.js'; -import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true, - - kind: 'write:user-groups', - - description: 'Invite a user to an existing group.', - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '583f8bc0-8eee-4b78-9299-1e14fc91e409', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: 'da52de61-002c-475b-90e1-ba64f9cf13a8', - }, - - alreadyAdded: { - message: 'That user has already been added to that group.', - code: 'ALREADY_ADDED', - id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c', - }, - - alreadyInvited: { - message: 'That user has already been invited to that group.', - code: 'ALREADY_INVITED', - id: 'ee0f58b4-b529-4d13-b761-b9a3e69f97e6', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId', 'userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - @Inject(DI.userGroupInvitationsRepository) - private userGroupInvitationsRepository: UserGroupInvitationsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private idService: IdService, - private getterService: GetterService, - private createNotificationService: CreateNotificationService, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await this.userGroupsRepository.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // Fetch the user - const user = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - const joining = await this.userGroupJoiningsRepository.findOneBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (joining) { - throw new ApiError(meta.errors.alreadyAdded); - } - - const existInvitation = await this.userGroupInvitationsRepository.findOneBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (existInvitation) { - throw new ApiError(meta.errors.alreadyInvited); - } - - const invitation = await this.userGroupInvitationsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: userGroup.id, - } as UserGroupInvitation).then(x => this.userGroupInvitationsRepository.findOneByOrFail(x.identifiers[0])); - - // 通知を作成 - this.createNotificationService.createNotification(user.id, 'groupInvited', { - notifierId: me.id, - userGroupInvitationId: invitation.id, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts deleted file mode 100644 index 8daee3a6f5..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Not, In } from 'typeorm'; -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['groups', 'account'], - - requireCredential: true, - - kind: 'read:user-groups', - - description: 'List the groups that the authenticated user is a member of.', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'UserGroup', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private userGroupEntityService: UserGroupEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const ownedGroups = await this.userGroupsRepository.findBy({ - userId: me.id, - }); - - const joinings = await this.userGroupJoiningsRepository.findBy({ - userId: me.id, - ...(ownedGroups.length > 0 ? { - userGroupId: Not(In(ownedGroups.map(x => x.id))), - } : {}), - }); - - return await Promise.all(joinings.map(x => this.userGroupEntityService.pack(x.userGroupId))); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts deleted file mode 100644 index 846f80e64d..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true, - - kind: 'write:user-groups', - - description: 'Leave a group. The owner of a group can not leave. They must transfer ownership or delete the group instead.', - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '62780270-1f67-5dc0-daca-3eb510612e31', - }, - - youAreOwner: { - message: 'Your are the owner.', - code: 'YOU_ARE_OWNER', - id: 'b6d6e0c2-ef8a-9bb8-653d-79f4a3107c69', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await this.userGroupsRepository.findOneBy({ - id: ps.groupId, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - if (me.id === userGroup.userId) { - throw new ApiError(meta.errors.youAreOwner); - } - - await this.userGroupJoiningsRepository.delete({ userGroupId: userGroup.id, userId: me.id }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts deleted file mode 100644 index 0bc6e8b3fc..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupsRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['groups', 'account'], - - requireCredential: true, - - kind: 'read:user-groups', - - description: 'List the groups that the authenticated user is the owner of.', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'UserGroup', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - private userGroupEntityService: UserGroupEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const userGroups = await this.userGroupsRepository.findBy({ - userId: me.id, - }); - - return await Promise.all(userGroups.map(x => this.userGroupEntityService.pack(x))); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts deleted file mode 100644 index 409006b0b0..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true, - - kind: 'write:user-groups', - - description: 'Removes a specified user from a group. The owner can not be removed.', - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '4662487c-05b1-4b78-86e5-fd46998aba74', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '0b5cc374-3681-41da-861e-8bc1146f7a55', - }, - - isOwner: { - message: 'The user is the owner.', - code: 'IS_OWNER', - id: '1546eed5-4414-4dea-81c1-b0aec4f6d2af', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId', 'userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private getterService: GetterService, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await this.userGroupsRepository.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // Fetch the user - const user = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - if (user.id === userGroup.userId) { - throw new ApiError(meta.errors.isOwner); - } - - // Pull the user - await this.userGroupJoiningsRepository.delete({ userGroupId: userGroup.id, userId: user.id }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts deleted file mode 100644 index 2b0f403f33..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['groups', 'account'], - - requireCredential: true, - - kind: 'read:user-groups', - - description: 'Show the properties of a group.', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'UserGroup', - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private userGroupEntityService: UserGroupEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await this.userGroupsRepository.findOneBy({ - id: ps.groupId, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - const joining = await this.userGroupJoiningsRepository.findOneBy({ - userId: me.id, - userGroupId: userGroup.id, - }); - - if (joining == null && userGroup.userId !== me.id) { - throw new ApiError(meta.errors.noSuchGroup); - } - - return await this.userGroupEntityService.pack(userGroup); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts deleted file mode 100644 index 3130d98ed1..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['groups', 'users'], - - requireCredential: true, - - kind: 'write:user-groups', - - description: 'Transfer ownership of a group from the authenticated user to another user.', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'UserGroup', - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '8e31d36b-2f88-4ccd-a438-e2d78a9162db', - }, - - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '711f7ebb-bbb9-4dfa-b540-b27809fed5e9', - }, - - noSuchGroupMember: { - message: 'No such group member.', - code: 'NO_SUCH_GROUP_MEMBER', - id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId', 'userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - private userGroupEntityService: UserGroupEntityService, - private getterService: GetterService, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await this.userGroupsRepository.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // Fetch the user - const user = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - const joining = await this.userGroupJoiningsRepository.findOneBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (joining == null) { - throw new ApiError(meta.errors.noSuchGroupMember); - } - - await this.userGroupsRepository.update(userGroup.id, { - userId: ps.userId, - }); - - return await this.userGroupEntityService.pack(userGroup.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts deleted file mode 100644 index 5af849de14..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupsRepository } from '@/models/index.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['groups'], - - requireCredential: true, - - kind: 'write:user-groups', - - description: 'Update the properties of a group.', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'UserGroup', - }, - - errors: { - noSuchGroup: { - message: 'No such group.', - code: 'NO_SUCH_GROUP', - id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - name: { type: 'string', minLength: 1, maxLength: 100 }, - }, - required: ['groupId', 'name'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.userGroupsRepository) - private userGroupsRepository: UserGroupsRepository, - - private userGroupEntityService: UserGroupEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await this.userGroupsRepository.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - await this.userGroupsRepository.update(userGroup.id, { - name: ps.name, - }); - - return await this.userGroupEntityService.pack(userGroup.id); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 3a079ee1ab..1c1fdc23f1 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -45,6 +45,12 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b', }, + + tooManyUsers: { + message: 'You can not push users any more.', + code: 'TOO_MANY_USERS', + id: '2dd9752e-a338-413d-8eec-41814430989b', + }, }, } as const; @@ -110,8 +116,15 @@ export default class extends Endpoint { throw new ApiError(meta.errors.alreadyAdded); } - // Push the user - await this.userListService.push(user, userList, me); + try { + await this.userListService.push(user, userList, me); + } catch (err) { + if (err instanceof UserListService.TooManyUsersError) { + throw new ApiError(meta.errors.tooManyUsers); + } + + throw err; + } }); } } diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index b176e6c65d..6c340d8fb2 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -1,7 +1,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; -import { USER_ACTIVE_THRESHOLD } from '@/const.js'; +import type { Config } from '@/config.js'; import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -31,20 +31,13 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, detail: { type: 'boolean', default: true }, + + username: { type: 'string', nullable: true }, + host: { type: 'string', nullable: true }, }, anyOf: [ - { - properties: { - username: { type: 'string', nullable: true }, - }, - required: ['username'] - }, - { - properties: { - host: { type: 'string', nullable: true }, - }, - required: ['host'] - }, + { required: ['username'] }, + { required: ['host'] }, ], } as const; @@ -54,6 +47,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -63,79 +59,76 @@ export default class extends Endpoint { private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 - - if (ps.host) { - const q = this.usersRepository.createQueryBuilder('user') - .where('user.isSuspended = FALSE') - .andWhere('user.host LIKE :host', { host: sqlLikeEscape(ps.host.toLowerCase()) + '%' }); - + const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => { if (ps.username) { - q.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }); + query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }); } - q.andWhere('user.updatedAt IS NOT NULL'); - q.orderBy('user.updatedAt', 'DESC'); - - const users = await q.take(ps.limit).getMany(); - - return await this.userEntityService.packMany(users, me, { detail: ps.detail }); - } else if (ps.username) { - let users: User[] = []; - - if (me) { - const followingQuery = this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - const query = this.usersRepository.createQueryBuilder('user') - .where(`user.id IN (${ followingQuery.getQuery() })`) - .andWhere('user.id != :meId', { meId: me.id }) - .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })); - - query.setParameters(followingQuery.getParameters()); - - users = await query - .orderBy('user.usernameLower', 'ASC') - .take(ps.limit) - .getMany(); - - if (users.length < ps.limit) { - const otherQuery = await this.usersRepository.createQueryBuilder('user') - .where(`user.id NOT IN (${ followingQuery.getQuery() })`) - .andWhere('user.id != :meId', { meId: me.id }) - .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }) - .andWhere('user.updatedAt IS NOT NULL'); - - otherQuery.setParameters(followingQuery.getParameters()); - - const otherUsers = await otherQuery - .orderBy('user.updatedAt', 'DESC') - .take(ps.limit - users.length) - .getMany(); - - users = users.concat(otherUsers); + if (ps.host) { + if (ps.host === this.config.hostname || ps.host === '.') { + query.andWhere('user.host IS NULL'); + } else { + query.andWhere('user.host LIKE :host', { + host: sqlLikeEscape(ps.host.toLowerCase()) + '%', + }); } - } else { - users = await this.usersRepository.createQueryBuilder('user') - .where('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }) - .andWhere('user.updatedAt IS NOT NULL') + } + + return query; + }; + + const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 + + let users: User[] = []; + + if (me) { + const followingQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const query = setUsernameAndHostQuery() + .andWhere(`user.id IN (${ followingQuery.getQuery() })`) + .andWhere('user.id != :meId', { meId: me.id }) + .andWhere('user.isSuspended = FALSE') + .andWhere(new Brackets(qb => { qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })); + + query.setParameters(followingQuery.getParameters()); + + users = await query + .orderBy('user.usernameLower', 'ASC') + .take(ps.limit) + .getMany(); + + if (users.length < ps.limit) { + const otherQuery = setUsernameAndHostQuery() + .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`) + .andWhere('user.isSuspended = FALSE') + .andWhere('user.updatedAt IS NOT NULL'); + + otherQuery.setParameters(followingQuery.getParameters()); + + const otherUsers = await otherQuery .orderBy('user.updatedAt', 'DESC') .take(ps.limit - users.length) .getMany(); - } - return await this.userEntityService.packMany(users, me, { detail: !!ps.detail }); + users = users.concat(otherUsers); + } + } else { + const query = setUsernameAndHostQuery() + .andWhere('user.isSuspended = FALSE') + .andWhere('user.updatedAt IS NOT NULL'); + + users = await query + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit - users.length) + .getMany(); } - return []; + return await this.userEntityService.packMany(users, me, { detail: !!ps.detail }); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 70258ef009..29f24b045a 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -54,32 +54,22 @@ export const meta = { export const paramDef = { type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + userIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, + }, anyOf: [ - { - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - properties: { - userIds: { type: 'array', uniqueItems: true, items: { - type: 'string', format: 'misskey:id', - } }, - }, - required: ['userIds'], - }, - { - properties: { - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', - }, - }, - required: ['username'], - }, + { required: ['userId'] }, + { required: ['userIds'] }, + { required: ['username'] }, ], } as const; diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 198fc190d4..f9ef8218c1 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -1,5 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; +import { Injectable } from '@nestjs/common'; +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'; @@ -11,11 +11,8 @@ import { ServerStatsChannelService } from './channels/server-stats.js'; import { QueueStatsChannelService } from './channels/queue-stats.js'; import { UserListChannelService } from './channels/user-list.js'; import { AntennaChannelService } from './channels/antenna.js'; -import { MessagingChannelService } from './channels/messaging.js'; -import { MessagingIndexChannelService } from './channels/messaging-index.js'; import { DriveChannelService } from './channels/drive.js'; import { HashtagChannelService } from './channels/hashtag.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ChannelsService { @@ -29,8 +26,6 @@ export class ChannelsService { private hashtagChannelService: HashtagChannelService, private antennaChannelService: AntennaChannelService, private channelChannelService: ChannelChannelService, - private messagingChannelService: MessagingChannelService, - private messagingIndexChannelService: MessagingIndexChannelService, private driveChannelService: DriveChannelService, private serverStatsChannelService: ServerStatsChannelService, private queueStatsChannelService: QueueStatsChannelService, @@ -50,8 +45,6 @@ export class ChannelsService { case 'hashtag': return this.hashtagChannelService; case 'antenna': return this.antennaChannelService; case 'channel': return this.channelChannelService; - case 'messaging': return this.messagingChannelService; - case 'messagingIndex': return this.messagingIndexChannelService; case 'drive': return this.driveChannelService; case 'serverStats': return this.serverStatsChannelService; case 'queueStats': return this.queueStatsChannelService; diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts index 210e016a7e..157fcd6aa3 100644 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ b/packages/backend/src/server/api/stream/channels/admin.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 44beef2da2..18604d94f0 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -1,5 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import { Injectable } from '@nestjs/common'; import { isUserRelated } from '@/misc/is-user-related.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 5ba84e43c4..f5ef1d1102 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -1,32 +1,24 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, UsersRepository } from '@/models/index.js'; +import { Injectable } from '@nestjs/common'; import { isUserRelated } from '@/misc/is-user-related.js'; -import type { User } from '@/models/entities/User.js'; import type { Packed } from '@/misc/schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; -import type { StreamMessages } from '../types.js'; class ChannelChannel extends Channel { public readonly chName = 'channel'; public static shouldShare = false; public static requireCredential = false; private channelId: string; - private typers: Record = {}; - private emitTypersIntervalId: ReturnType; constructor( private noteEntityService: NoteEntityService, - private userEntityService: UserEntityService, id: string, connection: Channel['connection'], ) { super(id, connection); //this.onNote = this.onNote.bind(this); - //this.emitTypers = this.emitTypers.bind(this); } @bindThis @@ -35,8 +27,6 @@ class ChannelChannel extends Channel { // Subscribe stream this.subscriber.on('notesStream', this.onNote); - this.subscriber.on(`channelStream:${this.channelId}`, this.onEvent); - this.emitTypersIntervalId = setInterval(this.emitTypers, 5000); } @bindThis @@ -66,42 +56,10 @@ class ChannelChannel extends Channel { this.send('note', note); } - @bindThis - private onEvent(data: StreamMessages['channel']['payload']) { - if (data.type === 'typing') { - const id = data.body; - const begin = this.typers[id] == null; - this.typers[id] = new Date(); - if (begin) { - this.emitTypers(); - } - } - } - - @bindThis - private async emitTypers() { - const now = new Date(); - - // Remove not typing users - for (const [userId, date] of Object.entries(this.typers)) { - if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; - } - - const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false }); - - this.send({ - type: 'typers', - body: users, - }); - } - @bindThis public dispose() { // Unsubscribe events this.subscriber.off('notesStream', this.onNote); - this.subscriber.off(`channelStream:${this.channelId}`, this.onEvent); - - clearInterval(this.emitTypersIntervalId); } } @@ -112,7 +70,6 @@ export class ChannelChannelService { constructor( private noteEntityService: NoteEntityService, - private userEntityService: UserEntityService, ) { } @@ -120,7 +77,6 @@ export class ChannelChannelService { public create(id: string, connection: Channel['connection']): ChannelChannel { return new ChannelChannel( this.noteEntityService, - this.userEntityService, id, connection, ); diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts index cfcb125b6b..52bb29fabe 100644 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ b/packages/backend/src/server/api/stream/channels/drive.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; 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 43d8907fc9..b8c0076ed9 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -1,5 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 073b737079..00f8d8ecd2 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -1,5 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import { Injectable } from '@nestjs/common'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/schema.js'; 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 5707ddd821..04a9f29686 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -1,5 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; 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 340f677815..ab52aabb30 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,10 +1,8 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import type { Packed } from '@/misc/schema.js'; -import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; 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 ea29e30d63..d8532c477b 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,5 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/schema.js'; diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 42f255b8fe..4dd16b530a 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -1,5 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +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'; diff --git a/packages/backend/src/server/api/stream/channels/messaging-index.ts b/packages/backend/src/server/api/stream/channels/messaging-index.ts deleted file mode 100644 index 66cb79f7a7..0000000000 --- a/packages/backend/src/server/api/stream/channels/messaging-index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; - -class MessagingIndexChannel extends Channel { - public readonly chName = 'messagingIndex'; - public static shouldShare = true; - public static requireCredential = true; - - @bindThis - public async init(params: any) { - // Subscribe messaging index stream - this.subscriber.on(`messagingIndexStream:${this.user!.id}`, data => { - this.send(data); - }); - } -} - -@Injectable() -export class MessagingIndexChannelService { - public readonly shouldShare = MessagingIndexChannel.shouldShare; - public readonly requireCredential = MessagingIndexChannel.requireCredential; - - constructor( - ) { - } - - @bindThis - public create(id: string, connection: Channel['connection']): MessagingIndexChannel { - return new MessagingIndexChannel( - id, - connection, - ); - } -} diff --git a/packages/backend/src/server/api/stream/channels/messaging.ts b/packages/backend/src/server/api/stream/channels/messaging.ts deleted file mode 100644 index 92af6b591c..0000000000 --- a/packages/backend/src/server/api/stream/channels/messaging.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UserGroupJoiningsRepository, UsersRepository, MessagingMessagesRepository } from '@/models/index.js'; -import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; -import type { UserGroup } from '@/models/entities/UserGroup.js'; -import { MessagingService } from '@/core/MessagingService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import Channel from '../channel.js'; -import type { StreamMessages } from '../types.js'; - -class MessagingChannel extends Channel { - public readonly chName = 'messaging'; - public static shouldShare = false; - public static requireCredential = true; - - private otherpartyId: string | null; - private otherparty: User | null; - private groupId: string | null; - private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`; - private typers: Record = {}; - private emitTypersIntervalId: ReturnType; - - constructor( - private usersRepository: UsersRepository, - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - private messagingMessagesRepository: MessagingMessagesRepository, - private userEntityService: UserEntityService, - private messagingService: MessagingService, - - id: string, - connection: Channel['connection'], - ) { - super(id, connection); - //this.onEvent = this.onEvent.bind(this); - //this.onMessage = this.onMessage.bind(this); - //this.emitTypers = this.emitTypers.bind(this); - } - - @bindThis - public async init(params: any) { - this.otherpartyId = params.otherparty; - this.otherparty = this.otherpartyId ? await this.usersRepository.findOneByOrFail({ id: this.otherpartyId }) : null; - this.groupId = params.group; - - // Check joining - if (this.groupId) { - const joining = await this.userGroupJoiningsRepository.findOneBy({ - userId: this.user!.id, - userGroupId: this.groupId, - }); - - if (joining == null) { - return; - } - } - - this.emitTypersIntervalId = setInterval(this.emitTypers, 5000); - - this.subCh = this.otherpartyId - ? `messagingStream:${this.user!.id}-${this.otherpartyId}` - : `messagingStream:${this.groupId}`; - - // Subscribe messaging stream - this.subscriber.on(this.subCh, this.onEvent); - } - - @bindThis - private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) { - if (data.type === 'typing') { - const id = data.body; - const begin = this.typers[id] == null; - this.typers[id] = new Date(); - if (begin) { - this.emitTypers(); - } - } else { - this.send(data); - } - } - - @bindThis - public onMessage(type: string, body: any) { - switch (type) { - case 'read': - if (this.otherpartyId) { - this.messagingService.readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]); - - // リモートユーザーからのメッセージだったら既読配信 - if (this.userEntityService.isLocalUser(this.user!) && this.userEntityService.isRemoteUser(this.otherparty!)) { - this.messagingMessagesRepository.findOneBy({ id: body.id }).then(message => { - if (message) this.messagingService.deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message); - }); - } - } else if (this.groupId) { - this.messagingService.readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]); - } - break; - } - } - - @bindThis - private async emitTypers() { - const now = new Date(); - - // Remove not typing users - for (const [userId, date] of Object.entries(this.typers)) { - if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; - } - - const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false }); - - this.send({ - type: 'typers', - body: users, - }); - } - - @bindThis - public dispose() { - this.subscriber.off(this.subCh, this.onEvent); - - clearInterval(this.emitTypersIntervalId); - } -} - -@Injectable() -export class MessagingChannelService { - public readonly shouldShare = MessagingChannel.shouldShare; - public readonly requireCredential = MessagingChannel.requireCredential; - - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userGroupJoiningsRepository) - private userGroupJoiningsRepository: UserGroupJoiningsRepository, - - @Inject(DI.messagingMessagesRepository) - private messagingMessagesRepository: MessagingMessagesRepository, - - private userEntityService: UserEntityService, - private messagingService: MessagingService, - ) { - } - - @bindThis - public create(id: string, connection: Channel['connection']): MessagingChannel { - return new MessagingChannel( - this.usersRepository, - this.userGroupJoiningsRepository, - this.messagingMessagesRepository, - this.userEntityService, - this.messagingService, - id, - connection, - ); - } -} 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 c773916103..7f48c54999 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -1,5 +1,5 @@ import Xev from 'xev'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; 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 492912dbe6..9eae0cf2d3 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -1,5 +1,5 @@ import Xev from 'xev'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; 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 16af32868c..7254d0a6d4 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListJoiningsRepository, UserListsRepository, NotesRepository } from '@/models/index.js'; +import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/schema.js'; diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index 6763953f9d..d3056aca57 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -3,7 +3,6 @@ import type { Channel as ChannelModel } from '@/models/entities/Channel.js'; import type { FollowingsRepository, MutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import type { UserProfile } from '@/models/entities/UserProfile.js'; -import type { UserGroup } from '@/models/entities/UserGroup.js'; import type { Packed } from '@/misc/schema.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { NoteReadService } from '@/core/NoteReadService.js'; @@ -147,12 +146,6 @@ export default class Connection { case 'disconnect': this.onChannelDisconnectRequested(body); break; case 'channel': this.onChannelMessageRequested(body); break; case 'ch': this.onChannelMessageRequested(body); break; // alias - - // 個々のチャンネルではなくルートレベルでこれらのメッセージを受け取る理由は、 - // クライアントの事情を考慮したとき、入力フォームはノートチャンネルやメッセージのメインコンポーネントとは別 - // なこともあるため、それらのコンポーネントがそれぞれ各チャンネルに接続するようにするのは面倒なため。 - case 'typingOnChannel': this.typingOnChannel(body.channel); break; - case 'typingOnMessaging': this.typingOnMessaging(body); break; } } @@ -325,24 +318,6 @@ export default class Connection { } } - @bindThis - private typingOnChannel(channel: ChannelModel['id']) { - if (this.user) { - this.globalEventService.publishChannelStream(channel, 'typing', this.user.id); - } - } - - @bindThis - private typingOnMessaging(param: { partner?: User['id']; group?: UserGroup['id']; }) { - if (this.user) { - if (param.partner) { - this.globalEventService.publishMessagingStream(param.partner, this.user.id, 'typing', this.user.id); - } else if (param.group) { - this.globalEventService.publishGroupMessagingStream(param.group, 'typing', this.user.id); - } - } - } - @bindThis private async updateFollowing() { const followings = await this.followingsRepository.find({ diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index 8bb4147b43..c450773055 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -6,15 +6,13 @@ import type { Antenna } from '@/models/entities/Antenna.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFolder } from '@/models/entities/DriveFolder.js'; import type { UserList } from '@/models/entities/UserList.js'; -import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; -import type { UserGroup } from '@/models/entities/UserGroup.js'; import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; import type { Signin } from '@/models/entities/Signin.js'; import type { Page } from '@/models/entities/Page.js'; import type { Packed } from '@/misc/schema.js'; import type { Webhook } from '@/models/entities/Webhook.js'; import type { Meta } from '@/models/entities/Meta.js'; -import { Following, Role, RoleAssignment } from '@/models'; +import { Role, RoleAssignment } from '@/models'; import type Emitter from 'strict-event-emitter-types'; import type { EventEmitter } from 'events'; @@ -44,10 +42,10 @@ export interface InternalStreamTypes { export interface BroadcastTypes { emojiAdded: { - emoji: Packed<'Emoji'>; + emoji: Packed<'EmojiDetailed'>; }; emojiUpdated: { - emojis: Packed<'Emoji'>[]; + emojis: Packed<'EmojiDetailed'>[]; }; emojiDeleted: { emojis: { @@ -96,9 +94,6 @@ export interface MainStreamTypes { readAllUnreadMentions: undefined; unreadSpecifiedNote: Note['id']; readAllUnreadSpecifiedNotes: undefined; - readAllMessagingMessages: undefined; - messagingMessage: Packed<'MessagingMessage'>; - unreadMessagingMessage: Packed<'MessagingMessage'>; readAllAntennas: undefined; unreadAntenna: Antenna; readAllAnnouncements: undefined; @@ -153,10 +148,6 @@ type NoteStreamEventTypes = { }; }; -export interface ChannelStreamTypes { - typing: User['id']; -} - export interface UserListStreamTypes { userAdded: Packed<'User'>; userRemoved: Packed<'User'>; @@ -166,28 +157,6 @@ export interface AntennaStreamTypes { note: Note; } -export interface MessagingStreamTypes { - read: MessagingMessage['id'][]; - typing: User['id']; - message: Packed<'MessagingMessage'>; - deleted: MessagingMessage['id']; -} - -export interface GroupMessagingStreamTypes { - read: { - ids: MessagingMessage['id'][]; - userId: User['id']; - }; - typing: User['id']; - message: Packed<'MessagingMessage'>; - deleted: MessagingMessage['id']; -} - -export interface MessagingIndexStreamTypes { - read: MessagingMessage['id'][]; - message: Packed<'MessagingMessage'>; -} - export interface AdminStreamTypes { newAbuseUserReport: { id: AbuseUserReport['id']; @@ -209,7 +178,14 @@ type EventUnionFromDictionary< // redis通すとDateのインスタンスはstringに変換されるので type Serialized = { - [K in keyof T]: T[K] extends Date ? string : T[K] extends Record ? Serialized : T[K]; + [K in keyof T]: + T[K] extends Date + ? string + : T[K] extends (Date | null) + ? (string | null) + : T[K] extends Record + ? Serialized + : T[K]; }; type SerializedAll = { @@ -242,10 +218,6 @@ export type StreamMessages = { name: `noteStream:${Note['id']}`; payload: EventUnionFromDictionary>; }; - channel: { - name: `channelStream:${Channel['id']}`; - payload: EventUnionFromDictionary>; - }; userList: { name: `userListStream:${UserList['id']}`; payload: EventUnionFromDictionary>; @@ -254,18 +226,6 @@ export type StreamMessages = { name: `antennaStream:${Antenna['id']}`; payload: EventUnionFromDictionary>; }; - messaging: { - name: `messagingStream:${User['id']}-${User['id']}`; - payload: EventUnionFromDictionary>; - }; - groupMessaging: { - name: `messagingStream:${UserGroup['id']}`; - payload: EventUnionFromDictionary>; - }; - messagingIndex: { - name: `messagingIndexStream:${User['id']}`; - payload: EventUnionFromDictionary>; - }; admin: { name: `adminStream:${User['id']}`; payload: EventUnionFromDictionary>; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 5bc5e1889f..94679c14ed 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -25,7 +25,7 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; -import type { ChannelsRepository, ClipsRepository, EmojisRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 57461b7a33..2ce7293a52 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import summaly from 'summaly'; +import { summaly } from 'summaly'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -30,7 +30,7 @@ export class UrlPreviewService { } @bindThis - private wrap(url?: string): string | null { + private wrap(url?: string | null): string | null { return url != null ? url.match(/^https?:\/\//) ? `${this.config.mediaProxy}/preview.webp?${query({ @@ -64,14 +64,21 @@ export class UrlPreviewService { ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`); try { - const summary = meta.summalyProxy ? await this.httpRequestService.getJson>(`${meta.summalyProxy}?${query({ - url: url, - lang: lang ?? 'ja-JP', - })}`) : await summaly.default(url, { - followRedirects: false, - lang: lang ?? 'ja-JP', - }); - + const summary = meta.summalyProxy ? + await this.httpRequestService.getJson>(`${meta.summalyProxy}?${query({ + url: url, + lang: lang ?? 'ja-JP', + })}`) + : + await summaly(url, { + followRedirects: false, + lang: lang ?? 'ja-JP', + agent: { + http: this.httpRequestService.httpAgent, + https: this.httpRequestService.httpsAgent, + }, + }); + this.logger.succ(`Got preview of ${url}: ${summary.title}`); if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/src/server/web/cli.js index 3dff1d4860..3467f7ac2a 100644 --- a/packages/backend/src/server/web/cli.js +++ b/packages/backend/src/server/web/cli.js @@ -11,6 +11,9 @@ window.onload = async () => { // Send request fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { + headers: { + 'Content-Type': 'application/json' + }, method: 'POST', body: JSON.stringify(data), credentials: 'omit', diff --git a/packages/backend/src/server/web/manifest.json b/packages/backend/src/server/web/manifest.json index 48030a2980..41171d62a1 100644 --- a/packages/backend/src/server/web/manifest.json +++ b/packages/backend/src/server/web/manifest.json @@ -9,16 +9,26 @@ { "src": "/static-assets/icons/192.png", "sizes": "192x192", - "type": "image/png" + "type": "image/png", + "purpose": "maskable" }, { "src": "/static-assets/icons/512.png", "sizes": "512x512", - "type": "image/png" + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/static-assets/splash.png", + "sizes": "300x300", + "type": "image/png", + "purpose": "any" } ], "share_target": { "action": "/share/", + "method": "GET", + "enctype": "application/x-www-form-urlencoded", "params": { "title": "title", "text": "text", diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 7e9e193362..7c6a1e5199 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,4 +1,5 @@ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app'] as const; +export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const; +export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; diff --git a/packages/backend/test/_e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts similarity index 94% rename from packages/backend/test/_e2e/api-visibility.ts rename to packages/backend/test/e2e/api-visibility.ts index d29b9acb3d..4e162f42d0 100644 --- a/packages/backend/test/_e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -1,18 +1,18 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, startServer, shutdownServer } from '../utils.js'; +import { signup, api, post, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('API visibility', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; beforeAll(async () => { p = await startServer(); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); describe('Note visibility', () => { @@ -60,7 +60,7 @@ describe('API visibility', () => { //#endregion const show = async (noteId: any, by: any) => { - return await request('/notes/show', { + return await api('/notes/show', { noteId, }, by); }; @@ -75,7 +75,7 @@ describe('API visibility', () => { target2 = await signup({ username: 'target2' }); // follow alice <= follower - await request('/following/create', { userId: alice.id }, follower); + await api('/following/create', { userId: alice.id }, follower); // normal posts pub = await post(alice, { text: 'x', visibility: 'public' }); @@ -413,21 +413,21 @@ describe('API visibility', () => { //#region HTL test('[HTL] public-post が 自分が見れる', async () => { - const res = await request('/notes/timeline', { limit: 100 }, alice); + const res = await api('/notes/timeline', { limit: 100 }, alice); assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id === pub.id); assert.strictEqual(notes[0].text, 'x'); }); test('[HTL] public-post が 非フォロワーから見れない', async () => { - const res = await request('/notes/timeline', { limit: 100 }, other); + const res = await api('/notes/timeline', { limit: 100 }, other); assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id === pub.id); assert.strictEqual(notes.length, 0); }); test('[HTL] followers-post が フォロワーから見れる', async () => { - const res = await request('/notes/timeline', { limit: 100 }, follower); + const res = await api('/notes/timeline', { limit: 100 }, follower); assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id === fol.id); assert.strictEqual(notes[0].text, 'x'); @@ -436,21 +436,21 @@ describe('API visibility', () => { //#region RTL test('[replies] followers-reply が フォロワーから見れる', async () => { - const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { - const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other); + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other); assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes.length, 0); }); test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { - const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target); + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target); assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); @@ -459,14 +459,14 @@ describe('API visibility', () => { //#region MTL test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { - const res = await request('/notes/mentions', { limit: 100 }, target); + const res = await api('/notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { - const res = await request('/notes/mentions', { limit: 100 }, target); + const res = await api('/notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id === folM.id); assert.strictEqual(notes[0].text, '@target x'); @@ -474,4 +474,4 @@ describe('API visibility', () => { //#endregion }); }); -*/ + diff --git a/packages/backend/test/_e2e/api.ts b/packages/backend/test/e2e/api.ts similarity index 55% rename from packages/backend/test/_e2e/api.ts rename to packages/backend/test/e2e/api.ts index 7542c34db0..6ceccf66eb 100644 --- a/packages/backend/test/_e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -1,11 +1,11 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from '../utils.js'; +import { signup, api, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('API', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; let alice: any; let bob: any; let carol: any; @@ -15,69 +15,69 @@ describe('API', () => { alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); describe('General validation', () => { - test('wrong type', async(async () => { - const res = await request('/test', { + test('wrong type', async () => { + const res = await api('/test', { required: true, string: 42, }); assert.strictEqual(res.status, 400); - })); + }); - test('missing require param', async(async () => { - const res = await request('/test', { + test('missing require param', async () => { + const res = await api('/test', { string: 'a', }); assert.strictEqual(res.status, 400); - })); + }); - test('invalid misskey:id (empty string)', async(async () => { - const res = await request('/test', { + test('invalid misskey:id (empty string)', async () => { + const res = await api('/test', { required: true, id: '', }); assert.strictEqual(res.status, 400); - })); + }); - test('valid misskey:id', async(async () => { - const res = await request('/test', { + test('valid misskey:id', async () => { + const res = await api('/test', { required: true, id: '8wvhjghbxu', }); assert.strictEqual(res.status, 200); - })); + }); - test('default value', async(async () => { - const res = await request('/test', { + test('default value', async () => { + const res = await api('/test', { required: true, string: 'a', }); assert.strictEqual(res.status, 200); assert.strictEqual(res.body.default, 'hello'); - })); + }); - test('can set null even if it has default value', async(async () => { - const res = await request('/test', { + test('can set null even if it has default value', async () => { + const res = await api('/test', { required: true, nullableDefault: null, }); assert.strictEqual(res.status, 200); assert.strictEqual(res.body.nullableDefault, null); - })); + }); - test('cannot set undefined if it has default value', async(async () => { - const res = await request('/test', { + test('cannot set undefined if it has default value', async () => { + const res = await api('/test', { required: true, nullableDefault: undefined, }); assert.strictEqual(res.status, 200); assert.strictEqual(res.body.nullableDefault, 'hello'); - })); + }); }); }); diff --git a/packages/backend/test/_e2e/block.ts b/packages/backend/test/e2e/block.ts similarity index 77% rename from packages/backend/test/_e2e/block.ts rename to packages/backend/test/e2e/block.ts index c5f43e153c..4e9030f85d 100644 --- a/packages/backend/test/_e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -1,11 +1,11 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, startServer, shutdownServer } from '../utils.js'; +import { signup, api, post, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('Block', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; // alice blocks bob let alice: any; @@ -17,14 +17,14 @@ describe('Block', () => { alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); test('Block作成', async () => { - const res = await request('/blocking/create', { + const res = await api('/blocking/create', { userId: bob.id, }, alice); @@ -32,7 +32,7 @@ describe('Block', () => { }); test('ブロックされているユーザーをフォローできない', async () => { - const res = await request('/following/create', { userId: alice.id }, bob); + const res = await api('/following/create', { userId: alice.id }, bob); assert.strictEqual(res.status, 400); assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); @@ -41,7 +41,7 @@ describe('Block', () => { test('ブロックされているユーザーにリアクションできない', async () => { const note = await post(alice, { text: 'hello' }); - const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); + const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); assert.strictEqual(res.status, 400); assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); @@ -50,7 +50,7 @@ describe('Block', () => { test('ブロックされているユーザーに返信できない', async () => { const note = await post(alice, { text: 'hello' }); - const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob); + const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); @@ -59,7 +59,7 @@ describe('Block', () => { test('ブロックされているユーザーのノートをRenoteできない', async () => { const note = await post(alice, { text: 'hello' }); - const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob); + const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); @@ -74,7 +74,7 @@ describe('Block', () => { const bobNote = await post(bob); const carolNote = await post(carol); - const res = await request('/notes/local-timeline', {}, bob); + const res = await api('/notes/local-timeline', {}, bob); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); diff --git a/packages/backend/test/_e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts similarity index 76% rename from packages/backend/test/_e2e/endpoints.ts rename to packages/backend/test/e2e/endpoints.ts index ea8433dfa2..e864eab6cb 100644 --- a/packages/backend/test/_e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -1,29 +1,35 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import * as openapi from '@redocly/openapi-core'; -import { startServer, signup, post, request, simpleGet, port, shutdownServer, api } from '../utils.js'; +// node-fetch only supports it's own Blob yet +// https://github.com/node-fetch/node-fetch/pull/1664 +import { Blob } from 'node-fetch'; +import { startServer, signup, post, api, uploadFile } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('Endpoints', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; let alice: any; let bob: any; + let carol: any; + let dave: any; beforeAll(async () => { p = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); - }, 1000 * 30); + carol = await signup({ username: 'carol' }); + dave = await signup({ username: 'dave' }); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); describe('signup', () => { test('不正なユーザー名でアカウントが作成できない', async () => { - const res = await request('api/signup', { + const res = await api('signup', { username: 'test.', password: 'test', }); @@ -31,7 +37,7 @@ describe('Endpoints', () => { }); test('空のパスワードでアカウントが作成できない', async () => { - const res = await request('api/signup', { + const res = await api('signup', { username: 'test', password: '', }); @@ -44,7 +50,7 @@ describe('Endpoints', () => { password: 'test1', }; - const res = await request('api/signup', me); + const res = await api('signup', me); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -52,7 +58,7 @@ describe('Endpoints', () => { }); test('同じユーザー名のアカウントは作成できない', async () => { - const res = await request('api/signup', { + const res = await api('signup', { username: 'test1', password: 'test1', }); @@ -63,7 +69,7 @@ describe('Endpoints', () => { describe('signin', () => { test('間違ったパスワードでサインインできない', async () => { - const res = await request('api/signin', { + const res = await api('signin', { username: 'test1', password: 'bar', }); @@ -72,7 +78,7 @@ describe('Endpoints', () => { }); test('クエリをインジェクションできない', async () => { - const res = await request('api/signin', { + const res = await api('signin', { username: 'test1', password: { $gt: '', @@ -83,7 +89,7 @@ describe('Endpoints', () => { }); test('正しい情報でサインインできる', async () => { - const res = await request('api/signin', { + const res = await api('signin', { username: 'test1', password: 'test1', }); @@ -111,11 +117,12 @@ describe('Endpoints', () => { assert.strictEqual(res.body.birthday, myBirthday); }); - test('名前を空白にできない', async () => { + test('名前を空白にできる', async () => { const res = await api('/i/update', { name: ' ', }, alice); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.name, ' '); }); test('誕生日の設定を削除できる', async () => { @@ -201,7 +208,6 @@ describe('Endpoints', () => { test('リアクションできる', async () => { const bobPost = await post(bob); - const alice = await signup({ username: 'alice' }); const res = await api('/notes/reactions/create', { noteId: bobPost.id, reaction: '🚀', @@ -214,7 +220,7 @@ describe('Endpoints', () => { }, alice); assert.strictEqual(resNote.status, 200); - assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]); + assert.strictEqual(resNote.body.reactions['🚀'], 1); }); test('自分の投稿にもリアクションできる', async () => { @@ -228,7 +234,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 204); }); - test('二重にリアクションできない', async () => { + test('二重にリアクションすると上書きされる', async () => { const bobPost = await post(bob); await api('/notes/reactions/create', { @@ -241,7 +247,14 @@ describe('Endpoints', () => { reaction: '🚀', }, alice); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 204); + + const resNote = await api('/notes/show', { + noteId: bobPost.id, + }, alice); + + assert.strictEqual(resNote.status, 200); + assert.deepStrictEqual(resNote.body.reactions, { '🚀': 1 }); }); test('存在しない投稿にはリアクションできない', async () => { @@ -369,57 +382,22 @@ describe('Endpoints', () => { }); }); - /* - describe('/i', () => { - test('', async () => { - }); - }); - */ -}); - -/* -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js'; - -describe('API: Endpoints', () => { - let p: childProcess.ChildProcess; - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }); - - after(async () => { - await shutdownServer(p); - }); - describe('drive', () => { test('ドライブ情報を取得できる', async () => { - await uploadFile({ - userId: alice.id, - size: 256 + await uploadFile(alice, { + blob: new Blob([new Uint8Array(256)]), }); - await uploadFile({ - userId: alice.id, - size: 512 + await uploadFile(alice, { + blob: new Blob([new Uint8Array(512)]), }); - await uploadFile({ - userId: alice.id, - size: 1024 + await uploadFile(alice, { + blob: new Blob([new Uint8Array(1024)]), }); const res = await api('/drive', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - expect(res.body).have.property('usage').eql(1792); - })); + expect(res.body).toHaveProperty('usage', 1792); + }); }); describe('drive/files/create', () => { @@ -428,454 +406,392 @@ describe('API: Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'Lenna.png'); - })); + assert.strictEqual(res.body.name, 'Lenna.jpg'); + }); test('ファイルに名前を付けられる', async () => { - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', alice.token) - .field('name', 'Belmond.png') - .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); + const res = await uploadFile(alice, { name: 'Belmond.png' }); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('Belmond.png'); - })); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'Belmond.png'); + }); test('ファイル無しで怒られる', async () => { const res = await api('/drive/files/create', {}, alice); assert.strictEqual(res.status, 400); - })); + }); test('SVGファイルを作成できる', async () => { - const res = await uploadFile(alice, __dirname + '/resources/image.svg'); + const res = await uploadFile(alice, { path: 'image.svg' }); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.name, 'image.svg'); assert.strictEqual(res.body.type, 'image/svg+xml'); - })); + }); }); describe('drive/files/update', () => { test('名前を更新できる', async () => { - const file = await uploadFile(alice); + const file = (await uploadFile(alice)).body; const newName = 'いちごパスタ.png'; const res = await api('/drive/files/update', { fileId: file.id, - name: newName + name: newName, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.name, newName); - })); + }); test('他人のファイルは更新できない', async () => { - const file = await uploadFile(bob); + const file = (await uploadFile(alice)).body; const res = await api('/drive/files/update', { fileId: file.id, - name: 'いちごパスタ.png' - }, alice); + name: 'いちごパスタ.png', + }, bob); assert.strictEqual(res.status, 400); - })); + }); test('親フォルダを更新できる', async () => { - const file = await uploadFile(alice); + const file = (await uploadFile(alice)).body; const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const res = await api('/drive/files/update', { fileId: file.id, - folderId: folder.id + folderId: folder.id, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.folderId, folder.id); - })); + }); test('親フォルダを無しにできる', async () => { - const file = await uploadFile(alice); + const file = (await uploadFile(alice)).body; const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; await api('/drive/files/update', { fileId: file.id, - folderId: folder.id + folderId: folder.id, }, alice); const res = await api('/drive/files/update', { fileId: file.id, - folderId: null + folderId: null, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.folderId, null); - })); + }); test('他人のフォルダには入れられない', async () => { - const file = await uploadFile(alice); + const file = (await uploadFile(alice)).body; const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, bob)).body; const res = await api('/drive/files/update', { fileId: file.id, - folderId: folder.id + folderId: folder.id, }, alice); assert.strictEqual(res.status, 400); - })); + }); test('存在しないフォルダで怒られる', async () => { - const file = await uploadFile(alice); + const file = (await uploadFile(alice)).body; const res = await api('/drive/files/update', { fileId: file.id, - folderId: '000000000000000000000000' + folderId: '000000000000000000000000', }, alice); assert.strictEqual(res.status, 400); - })); + }); test('不正なフォルダIDで怒られる', async () => { - const file = await uploadFile(alice); + const file = (await uploadFile(alice)).body; const res = await api('/drive/files/update', { fileId: file.id, - folderId: 'foo' + folderId: 'foo', }, alice); assert.strictEqual(res.status, 400); - })); + }); test('ファイルが存在しなかったら怒る', async () => { const res = await api('/drive/files/update', { fileId: '000000000000000000000000', - name: 'いちごパスタ.png' + name: 'いちごパスタ.png', }, alice); assert.strictEqual(res.status, 400); - })); + }); test('間違ったIDで怒られる', async () => { const res = await api('/drive/files/update', { fileId: 'kyoppie', - name: 'いちごパスタ.png' + name: 'いちごパスタ.png', }, alice); assert.strictEqual(res.status, 400); - })); + }); }); describe('drive/folders/create', () => { test('フォルダを作成できる', async () => { const res = await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.name, 'test'); - })); + }); }); describe('drive/folders/update', () => { test('名前を更新できる', async () => { const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const res = await api('/drive/folders/update', { folderId: folder.id, - name: 'new name' + name: 'new name', }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.name, 'new name'); - })); + }); test('他人のフォルダを更新できない', async () => { const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, bob)).body; const res = await api('/drive/folders/update', { folderId: folder.id, - name: 'new name' + name: 'new name', }, alice); assert.strictEqual(res.status, 400); - })); + }); test('親フォルダを更新できる', async () => { const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const parentFolder = (await api('/drive/folders/create', { - name: 'parent' + name: 'parent', }, alice)).body; const res = await api('/drive/folders/update', { folderId: folder.id, - parentId: parentFolder.id + parentId: parentFolder.id, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.parentId, parentFolder.id); - })); + }); test('親フォルダを無しに更新できる', async () => { const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const parentFolder = (await api('/drive/folders/create', { - name: 'parent' + name: 'parent', }, alice)).body; await api('/drive/folders/update', { folderId: folder.id, - parentId: parentFolder.id + parentId: parentFolder.id, }, alice); const res = await api('/drive/folders/update', { folderId: folder.id, - parentId: null + parentId: null, }, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.parentId, null); - })); + }); test('他人のフォルダを親フォルダに設定できない', async () => { const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const parentFolder = (await api('/drive/folders/create', { - name: 'parent' + name: 'parent', }, bob)).body; const res = await api('/drive/folders/update', { folderId: folder.id, - parentId: parentFolder.id + parentId: parentFolder.id, }, alice); assert.strictEqual(res.status, 400); - })); + }); test('フォルダが循環するような構造にできない', async () => { const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const parentFolder = (await api('/drive/folders/create', { - name: 'parent' + name: 'parent', }, alice)).body; await api('/drive/folders/update', { folderId: parentFolder.id, - parentId: folder.id + parentId: folder.id, }, alice); const res = await api('/drive/folders/update', { folderId: folder.id, - parentId: parentFolder.id + parentId: parentFolder.id, }, alice); assert.strictEqual(res.status, 400); - })); + }); test('フォルダが循環するような構造にできない(再帰的)', async () => { const folderA = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const folderB = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const folderC = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; await api('/drive/folders/update', { folderId: folderB.id, - parentId: folderA.id + parentId: folderA.id, }, alice); await api('/drive/folders/update', { folderId: folderC.id, - parentId: folderB.id + parentId: folderB.id, }, alice); const res = await api('/drive/folders/update', { folderId: folderA.id, - parentId: folderC.id + parentId: folderC.id, }, alice); assert.strictEqual(res.status, 400); - })); + }); test('フォルダが循環するような構造にできない(自身)', async () => { const folderA = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const res = await api('/drive/folders/update', { folderId: folderA.id, - parentId: folderA.id + parentId: folderA.id, }, alice); assert.strictEqual(res.status, 400); - })); + }); test('存在しない親フォルダを設定できない', async () => { const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const res = await api('/drive/folders/update', { folderId: folder.id, - parentId: '000000000000000000000000' + parentId: '000000000000000000000000', }, alice); assert.strictEqual(res.status, 400); - })); + }); test('不正な親フォルダIDで怒られる', async () => { const folder = (await api('/drive/folders/create', { - name: 'test' + name: 'test', }, alice)).body; const res = await api('/drive/folders/update', { folderId: folder.id, - parentId: 'foo' + parentId: 'foo', }, alice); assert.strictEqual(res.status, 400); - })); + }); test('存在しないフォルダを更新できない', async () => { const res = await api('/drive/folders/update', { - folderId: '000000000000000000000000' + folderId: '000000000000000000000000', }, alice); assert.strictEqual(res.status, 400); - })); + }); test('不正なフォルダIDで怒られる', async () => { const res = await api('/drive/folders/update', { - folderId: 'foo' + folderId: 'foo', }, alice); assert.strictEqual(res.status, 400); - })); - }); - - describe('messaging/messages/create', () => { - test('メッセージを送信できる', async () => { - const res = await api('/messaging/messages/create', { - userId: bob.id, - text: 'test' - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.text, 'test'); - })); - - test('自分自身にはメッセージを送信できない', async () => { - const res = await api('/messaging/messages/create', { - userId: alice.id, - text: 'Yo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('存在しないユーザーにはメッセージを送信できない', async () => { - const res = await api('/messaging/messages/create', { - userId: '000000000000000000000000', - text: 'test' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('不正なユーザーIDで怒られる', async () => { - const res = await api('/messaging/messages/create', { - userId: 'foo', - text: 'test' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('テキストが無くて怒られる', async () => { - const res = await api('/messaging/messages/create', { - userId: bob.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('文字数オーバーで怒られる', async () => { - const res = await api('/messaging/messages/create', { - userId: bob.id, - text: '!'.repeat(1001) - }, alice); - - assert.strictEqual(res.status, 400); - })); + }); }); describe('notes/replies', () => { test('自分に閲覧権限のない投稿は含まれない', async () => { const alicePost = await post(alice, { - text: 'foo' + text: 'foo', }); await post(bob, { replyId: alicePost.id, text: 'bar', visibility: 'specified', - visibleUserIds: [alice.id] + visibleUserIds: [alice.id], }); const res = await api('/notes/replies', { - noteId: alicePost.id + noteId: alicePost.id, }, carol); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(res.body.length, 0); - })); + }); }); describe('notes/timeline', () => { test('フォロワー限定投稿が含まれる', async () => { await api('/following/create', { - userId: alice.id - }, bob); + userId: carol.id, + }, dave); - const alicePost = await post(alice, { + const carolPost = await post(carol, { text: 'foo', - visibility: 'followers' + visibility: 'followers', }); - const res = await api('/notes/timeline', {}, bob); + const res = await api('/notes/timeline', {}, dave); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(res.body.length, 1); - assert.strictEqual(res.body[0].id, alicePost.id); - })); + assert.strictEqual(res.body[0].id, carolPost.id); + }); }); }); -*/ diff --git a/packages/backend/test/_e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts similarity index 83% rename from packages/backend/test/_e2e/fetch-resource.ts rename to packages/backend/test/e2e/fetch-resource.ts index 7ae133496a..6b3c795235 100644 --- a/packages/backend/test/_e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -1,9 +1,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import * as openapi from '@redocly/openapi-core'; -import { startServer, signup, post, request, simpleGet, port, shutdownServer } from '../utils.js'; +import { startServer, signup, post, api, simpleGet } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; // Request Accept const ONLY_AP = 'application/activity+json'; @@ -11,13 +10,12 @@ const PREFER_AP = 'application/activity+json, */*'; const PREFER_HTML = 'text/html, */*'; const UNSPECIFIED = '*/*'; -// Response Contet-Type +// Response Content-Type const AP = 'application/activity+json; charset=utf-8'; -const JSON = 'application/json; charset=utf-8'; const HTML = 'text/html; charset=utf-8'; describe('Fetch resource', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; let alice: any; let alicesPost: any; @@ -28,15 +26,15 @@ describe('Fetch resource', () => { alicesPost = await post(alice, { text: 'test', }); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); describe('Common', () => { test('meta', async () => { - const res = await request('/meta', { + const res = await api('/meta', { }); assert.strictEqual(res.status, 200); @@ -54,36 +52,26 @@ describe('Fetch resource', () => { assert.strictEqual(res.type, HTML); }); - test('GET api-doc', async () => { + test('GET api-doc (廃止)', async () => { const res = await simpleGet('/api-doc'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); + assert.strictEqual(res.status, 404); }); - test('GET api.json', async () => { + test('GET api.json (廃止)', async () => { const res = await simpleGet('/api.json'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, JSON); + assert.strictEqual(res.status, 404); }); - test('Validate api.json', async () => { - const config = await openapi.loadConfig(); - const result = await openapi.bundle({ - config, - ref: `http://localhost:${port}/api.json`, - }); - - for (const problem of result.problems) { - console.log(`${problem.message} - ${problem.location[0]?.pointer}`); - } - - assert.strictEqual(result.problems.length, 0); + test('GET api/foo (存在しない)', async () => { + const res = await simpleGet('/api/foo'); + assert.strictEqual(res.status, 404); + assert.strictEqual(res.body.error.code, 'UNKNOWN_API_ENDPOINT'); }); test('GET favicon.ico', async () => { const res = await simpleGet('/favicon.ico'); assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/x-icon'); + assert.strictEqual(res.type, 'image/vnd.microsoft.icon'); }); test('GET apple-touch-icon.png', async () => { diff --git a/packages/backend/test/_e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts similarity index 70% rename from packages/backend/test/_e2e/ff-visibility.ts rename to packages/backend/test/e2e/ff-visibility.ts index 84a5b5ef28..d53919ca1e 100644 --- a/packages/backend/test/_e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -1,36 +1,34 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from '../utils.js'; +import { signup, api, startServer, simpleGet } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('FF visibility', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; let alice: any; let bob: any; - let carol: any; beforeAll(async () => { p = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { - await request('/i/update', { + await api('/i/update', { ffVisibility: 'public', }, alice); - const followingRes = await request('/users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await request('/users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -41,14 +39,14 @@ describe('FF visibility', () => { }); test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { - await request('/i/update', { + await api('/i/update', { ffVisibility: 'followers', }, alice); - const followingRes = await request('/users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); - const followersRes = await request('/users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); @@ -59,14 +57,14 @@ describe('FF visibility', () => { }); test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { - await request('/i/update', { + await api('/i/update', { ffVisibility: 'followers', }, alice); - const followingRes = await request('/users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await request('/users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -75,18 +73,18 @@ describe('FF visibility', () => { }); test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { - await request('/i/update', { + await api('/i/update', { ffVisibility: 'followers', }, alice); - await request('/following/create', { + await api('/following/create', { userId: alice.id, }, bob); - const followingRes = await request('/users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await request('/users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -97,14 +95,14 @@ describe('FF visibility', () => { }); test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => { - await request('/i/update', { + await api('/i/update', { ffVisibility: 'private', }, alice); - const followingRes = await request('/users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, alice); - const followersRes = await request('/users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, alice); @@ -115,14 +113,14 @@ describe('FF visibility', () => { }); test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => { - await request('/i/update', { + await api('/i/update', { ffVisibility: 'private', }, alice); - const followingRes = await request('/users/following', { + const followingRes = await api('/users/following', { userId: alice.id, }, bob); - const followersRes = await request('/users/followers', { + const followersRes = await api('/users/followers', { userId: alice.id, }, bob); @@ -133,7 +131,7 @@ describe('FF visibility', () => { describe('AP', () => { test('ffVisibility が public 以外ならばAPからは取得できない', async () => { { - await request('/i/update', { + await api('/i/update', { ffVisibility: 'public', }, alice); @@ -143,22 +141,22 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 200); } { - await request('/i/update', { + await api('/i/update', { ffVisibility: 'followers', }, alice); - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + 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 request('/i/update', { + await api('/i/update', { ffVisibility: 'private', }, alice); - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + 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/mute.ts b/packages/backend/test/e2e/mute.ts similarity index 82% rename from packages/backend/test/_e2e/mute.ts rename to packages/backend/test/e2e/mute.ts index 8f7f72bb97..6654a290be 100644 --- a/packages/backend/test/_e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -1,11 +1,11 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, react, startServer, shutdownServer, waitFire } from '../utils.js'; +import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('Mute', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; // alice mutes carol let alice: any; @@ -17,14 +17,14 @@ describe('Mute', () => { alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); test('ミュート作成', async () => { - const res = await request('/mute/create', { + const res = await api('/mute/create', { userId: carol.id, }, alice); @@ -35,7 +35,7 @@ describe('Mute', () => { const bobNote = await post(bob, { text: '@alice hi' }); const carolNote = await post(carol, { text: '@alice hi' }); - const res = await request('/notes/mentions', {}, alice); + const res = await api('/notes/mentions', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); @@ -45,11 +45,11 @@ describe('Mute', () => { test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); await post(carol, { text: '@alice hi' }); - const res = await request('/i', {}, alice); + const res = await api('/i', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(res.body.hasUnreadMentions, false); @@ -57,7 +57,7 @@ describe('Mute', () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); @@ -66,8 +66,8 @@ describe('Mute', () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - await request('/notifications/mark-all-as-read', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); + await api('/notifications/mark-all-as-read', {}, alice); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); @@ -80,7 +80,7 @@ describe('Mute', () => { const bobNote = await post(bob); const carolNote = await post(carol); - const res = await request('/notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); @@ -96,7 +96,7 @@ describe('Mute', () => { renoteId: carolNote.id, }); - const res = await request('/notes/local-timeline', {}, alice); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); @@ -112,7 +112,7 @@ describe('Mute', () => { await react(bob, aliceNote, 'like'); await react(carol, aliceNote, 'like'); - const res = await request('/i/notifications', {}, alice); + const res = await api('/i/notifications', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); diff --git a/packages/backend/test/_e2e/note.ts b/packages/backend/test/e2e/note.ts similarity index 74% rename from packages/backend/test/_e2e/note.ts rename to packages/backend/test/e2e/note.ts index 47af6808f6..98ee34d8d1 100644 --- a/packages/backend/test/_e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -1,12 +1,12 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { Note } from '../../src/models/entities/note.js'; -import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from '../utils.js'; +import { Note } from '@/models/entities/Note.js'; +import { signup, post, uploadUrl, startServer, initTestDb, api } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('Note', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; let Notes: any; let alice: any; @@ -18,10 +18,10 @@ describe('Note', () => { Notes = connection.getRepository(Note); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); test('投稿できる', async () => { @@ -29,7 +29,7 @@ describe('Note', () => { text: 'test', }; - const res = await request('/notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -39,7 +39,7 @@ describe('Note', () => { test('ファイルを添付できる', async () => { const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - const res = await request('/notes/create', { + const res = await api('/notes/create', { fileIds: [file.id], }, alice); @@ -48,37 +48,37 @@ describe('Note', () => { assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]); }, 1000 * 10); - test('他人のファイルは無視', async () => { + test('他人のファイルで怒られる', async () => { const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - const res = await request('/notes/create', { + const res = await api('/notes/create', { text: 'test', fileIds: [file.id], }, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }, 1000 * 10); - test('存在しないファイルは無視', async () => { - const res = await request('/notes/create', { + test('存在しないファイルで怒られる', async () => { + const res = await api('/notes/create', { text: 'test', fileIds: ['000000000000000000000000'], }, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }); - test('不正なファイルIDは無視', async () => { - const res = await request('/notes/create', { + test('不正なファイルIDで怒られる', async () => { + const res = await api('/notes/create', { fileIds: ['kyoppie'], }, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }); test('返信できる', async () => { @@ -91,7 +91,7 @@ describe('Note', () => { replyId: bobPost.id, }; - const res = await request('/notes/create', alicePost, alice); + const res = await api('/notes/create', alicePost, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -109,7 +109,7 @@ describe('Note', () => { renoteId: bobPost.id, }; - const res = await request('/notes/create', alicePost, alice); + const res = await api('/notes/create', alicePost, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -127,7 +127,7 @@ describe('Note', () => { renoteId: bobPost.id, }; - const res = await request('/notes/create', alicePost, alice); + const res = await api('/notes/create', alicePost, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -140,7 +140,7 @@ describe('Note', () => { const post = { text: '!'.repeat(3000), }; - const res = await request('/notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); }); @@ -148,7 +148,7 @@ describe('Note', () => { const post = { text: '!'.repeat(3001), }; - const res = await request('/notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -157,7 +157,7 @@ describe('Note', () => { text: 'test', replyId: '000000000000000000000000', }; - const res = await request('/notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -165,7 +165,7 @@ describe('Note', () => { const post = { renoteId: '000000000000000000000000', }; - const res = await request('/notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -174,7 +174,7 @@ describe('Note', () => { text: 'test', replyId: 'foo', }; - const res = await request('/notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -182,7 +182,7 @@ describe('Note', () => { const post = { renoteId: 'foo', }; - const res = await request('/notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); }); @@ -191,7 +191,7 @@ describe('Note', () => { text: '@ghost yo', }; - const res = await request('/notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -203,7 +203,7 @@ describe('Note', () => { text: '@bob @bob @bob yo', }; - const res = await request('/notes/create', post, alice); + const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); @@ -215,7 +215,7 @@ describe('Note', () => { describe('notes/create', () => { test('投票を添付できる', async () => { - const res = await request('/notes/create', { + const res = await api('/notes/create', { text: 'test', poll: { choices: ['foo', 'bar'], @@ -228,14 +228,14 @@ describe('Note', () => { }); test('投票の選択肢が無くて怒られる', async () => { - const res = await request('/notes/create', { + const res = await api('/notes/create', { poll: {}, }, alice); assert.strictEqual(res.status, 400); }); test('投票の選択肢が無くて怒られる (空の配列)', async () => { - const res = await request('/notes/create', { + const res = await api('/notes/create', { poll: { choices: [], }, @@ -244,7 +244,7 @@ describe('Note', () => { }); test('投票の選択肢が1つで怒られる', async () => { - const res = await request('/notes/create', { + const res = await api('/notes/create', { poll: { choices: ['Strawberry Pasta'], }, @@ -253,14 +253,14 @@ describe('Note', () => { }); test('投票できる', async () => { - const { body } = await request('/notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], }, }, alice); - const res = await request('/notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 1, }, alice); @@ -269,19 +269,19 @@ describe('Note', () => { }); test('複数投票できない', async () => { - const { body } = await request('/notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], }, }, alice); - await request('/notes/polls/vote', { + await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 0, }, alice); - const res = await request('/notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 2, }, alice); @@ -290,7 +290,7 @@ describe('Note', () => { }); test('許可されている場合は複数投票できる', async () => { - const { body } = await request('/notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], @@ -298,17 +298,17 @@ describe('Note', () => { }, }, alice); - await request('/notes/polls/vote', { + await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 0, }, alice); - await request('/notes/polls/vote', { + await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 1, }, alice); - const res = await request('/notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 2, }, alice); @@ -317,7 +317,7 @@ describe('Note', () => { }); test('締め切られている場合は投票できない', async () => { - const { body } = await request('/notes/create', { + const { body } = await api('/notes/create', { text: 'test', poll: { choices: ['sakura', 'izumi', 'ako'], @@ -327,7 +327,7 @@ describe('Note', () => { await new Promise(x => setTimeout(x, 2)); - const res = await request('/notes/polls/vote', { + const res = await api('/notes/polls/vote', { noteId: body.createdNote.id, choice: 1, }, alice); diff --git a/packages/backend/test/_e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts similarity index 92% rename from packages/backend/test/_e2e/streaming.ts rename to packages/backend/test/e2e/streaming.ts index 790451d9b4..23c431f2e7 100644 --- a/packages/backend/test/_e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -1,12 +1,12 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { Following } from '../../src/models/entities/following.js'; -import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from '../utils.js'; +import { Following } from '@/models/entities/Following.js'; +import { connectStream, signup, api, post, startServer, initTestDb, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('Streaming', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; let Followings: any; const follow = async (follower: any, followee: any) => { @@ -71,10 +71,10 @@ describe('Streaming', () => { listId: list.id, userId: kyoko.id, }, chitose); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); describe('Events', () => { @@ -404,43 +404,45 @@ describe('Streaming', () => { }); })); - test('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type === 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - } - }, { - q: [ - ['foo', 'bar'], - ], - }); - - post(chitose, { - text: '#foo', - }); - - post(chitose, { - text: '#bar', - }); - - post(chitose, { - text: '#foo #bar', - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 0); - assert.strictEqual(barCount, 0); - assert.strictEqual(fooBarCount, 1); - ws.close(); - done(); - }, 3000); - })); + // XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac" + + // test('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { + // let fooCount = 0; + // let barCount = 0; + // let fooBarCount = 0; + + // const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { + // if (type === 'note') { + // if (body.text === '#foo') fooCount++; + // if (body.text === '#bar') barCount++; + // if (body.text === '#foo #bar') fooBarCount++; + // } + // }, { + // q: [ + // ['foo', 'bar'], + // ], + // }); + + // post(chitose, { + // text: '#foo', + // }); + + // post(chitose, { + // text: '#bar', + // }); + + // post(chitose, { + // text: '#foo #bar', + // }); + + // setTimeout(() => { + // assert.strictEqual(fooCount, 0); + // assert.strictEqual(barCount, 0); + // assert.strictEqual(fooBarCount, 1); + // ws.close(); + // done(); + // }, 3000); + // })); test('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => { let fooCount = 0; diff --git a/packages/backend/test/_e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts similarity index 78% rename from packages/backend/test/_e2e/thread-mute.ts rename to packages/backend/test/e2e/thread-mute.ts index 890b52a8c1..792436d88f 100644 --- a/packages/backend/test/_e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -1,11 +1,11 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, react, connectStream, startServer, shutdownServer } from '../utils.js'; +import { signup, api, post, connectStream, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('Note thread mute', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; let alice: any; let bob: any; @@ -16,22 +16,22 @@ describe('Note thread mute', () => { alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async () => { - await shutdownServer(p); + await p.close(); }); test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => { const bobNote = await post(bob, { text: '@alice @carol root note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - const res = await request('/notes/mentions', {}, alice); + const res = await api('/notes/mentions', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); @@ -42,27 +42,27 @@ describe('Note thread mute', () => { test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); const bobNote = await post(bob, { text: '@alice @carol root note' }); - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - const res = await request('/i', {}, alice); + const res = await api('/i', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(res.body.hasUnreadMentions, false); }); - test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { + test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); + await api('/i/read-all-unread-notes', {}, alice); const bobNote = await post(bob, { text: '@alice @carol root note' }); - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); let fired = false; @@ -86,12 +86,12 @@ describe('Note thread mute', () => { const bobNote = await post(bob, { text: '@alice @carol root note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - const res = await request('/i/notifications', {}, alice); + const res = await api('/i/notifications', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); diff --git a/packages/backend/test/_e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts similarity index 84% rename from packages/backend/test/_e2e/user-notes.ts rename to packages/backend/test/e2e/user-notes.ts index a6cc1057f9..690cba1746 100644 --- a/packages/backend/test/_e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -1,11 +1,11 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, uploadUrl, startServer, shutdownServer } from '../utils.js'; +import { signup, api, post, uploadUrl, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; describe('users/notes', () => { - let p: childProcess.ChildProcess; + let p: INestApplicationContext; let alice: any; let jpgNote: any; @@ -26,14 +26,14 @@ describe('users/notes', () => { jpgPngNote = await post(alice, { fileIds: [jpg.id, png.id], }); - }, 1000 * 30); + }, 1000 * 60 * 2); afterAll(async() => { - await shutdownServer(p); + await p.close(); }); test('ファイルタイプ指定 (jpg)', async () => { - const res = await request('/users/notes', { + const res = await api('/users/notes', { userId: alice.id, fileType: ['image/jpeg'], }, alice); @@ -46,7 +46,7 @@ describe('users/notes', () => { }); test('ファイルタイプ指定 (jpg or png)', async () => { - const res = await request('/users/notes', { + const res = await api('/users/notes', { userId: alice.id, fileType: ['image/jpeg', 'image/png'], }, alice); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 9efed267ea..6b31e68616 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -1,5 +1,16 @@ -import Resolver from '../../src/activitypub/resolver.js'; -import { IObject } from '../../src/activitypub/type.js'; +import type { Config } from '@/config.js'; +import type { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; +import type { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import type { ApRequestService } from '@/core/activitypub/ApRequestService.js'; +import { Resolver } from '@/core/activitypub/ApResolverService.js'; +import type { IObject } from '@/core/activitypub/type.js'; +import type { HttpRequestService } from '@/core/HttpRequestService.js'; +import type { InstanceActorService } from '@/core/InstanceActorService.js'; +import type { LoggerService } from '@/core/LoggerService.js'; +import type { MetaService } from '@/core/MetaService.js'; +import type { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; +import type { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository } from '@/models/index.js'; type MockResponse = { type: string; @@ -8,6 +19,25 @@ type MockResponse = { export class MockResolver extends Resolver { private _rs = new Map(); + + constructor(loggerService: LoggerService) { + super( + {} as Config, + {} as UsersRepository, + {} as NotesRepository, + {} as PollsRepository, + {} as NoteReactionsRepository, + {} as UtilityService, + {} as InstanceActorService, + {} as MetaService, + {} as ApRequestService, + {} as HttpRequestService, + {} as ApRendererService, + {} as ApDbResolverService, + loggerService, + ); + } + public async _register(uri: string, content: string | Record, type = 'application/activity+json') { this._rs.set(uri, { type, diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts new file mode 100644 index 0000000000..1f4a2dbc95 --- /dev/null +++ b/packages/backend/test/prelude/get-api-validator.ts @@ -0,0 +1,11 @@ +import { Schema } from '@/misc/schema'; +import Ajv from 'ajv'; + +export const getValidator = (paramDef: Schema) => { + const ajv = new Ajv({ + useDefaults: true, + }); + ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); + + return ajv.compile(paramDef); +} diff --git a/packages/backend/test/resources/misskey.svg b/packages/backend/test/resources/misskey.svg new file mode 100644 index 0000000000..3fcb2d3ecb Binary files /dev/null and b/packages/backend/test/resources/misskey.svg differ diff --git a/packages/backend/test/tests/mfm.ts b/packages/backend/test/tests/mfm.ts deleted file mode 100644 index 884f39d7fb..0000000000 --- a/packages/backend/test/tests/mfm.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as assert from 'assert'; -import * as mfm from 'mfm-js'; - -import { toHtml } from '../../src/mfm/to-html.js'; -import { fromHtml } from '../../src/mfm/from-html.js'; - -describe('toHtml', () => { - test('br', () => { - const input = 'foo\nbar\nbaz'; - const output = '

foo
bar
baz

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

foo
bar
baz

'; - assert.equal(toHtml(mfm.parse(input)), output); - }); -}); - -describe('fromHtml', () => { - test('p', () => { - assert.deepStrictEqual(fromHtml('

a

b

'), 'a\n\nb'); - }); - - test('block element', () => { - assert.deepStrictEqual(fromHtml('
a
b
'), 'a\nb'); - }); - - test('inline element', () => { - assert.deepStrictEqual(fromHtml('
  • a
  • b
'), 'a\nb'); - }); - - test('block code', () => { - assert.deepStrictEqual(fromHtml('
a\nb
'), '```\na\nb\n```'); - }); - - test('inline code', () => { - assert.deepStrictEqual(fromHtml('a'), '`a`'); - }); - - test('quote', () => { - assert.deepStrictEqual(fromHtml('
a\nb
'), '> a\n> b'); - }); - - test('br', () => { - assert.deepStrictEqual(fromHtml('

abc

d

'), 'abc\n\nd'); - }); - - test('link with different text', () => { - assert.deepStrictEqual(fromHtml('
'), 'a [c](https://example.com/b) d'); - }); - - test('link with different text, but not encoded', () => { - assert.deepStrictEqual(fromHtml('

a c d

'), 'a [c]() d'); - }); - - test('link with same text', () => { - assert.deepStrictEqual(fromHtml('

a https://example.com/b d

'), 'a https://example.com/b d'); - }); - - test('link with same text, but not encoded', () => { - assert.deepStrictEqual(fromHtml('

a https://example.com/ä d

'), 'a d'); - }); - - test('link with no url', () => { - assert.deepStrictEqual(fromHtml('

a c d

'), 'a [c](b) d'); - }); - - test('link without href', () => { - assert.deepStrictEqual(fromHtml('

a c d

'), 'a c d'); - }); - - test('link without text', () => { - assert.deepStrictEqual(fromHtml('

a d

'), 'a https://example.com/b d'); - }); - - test('link without both', () => { - assert.deepStrictEqual(fromHtml('

a d

'), 'a d'); - }); - - test('mention', () => { - assert.deepStrictEqual(fromHtml('

a @user d

'), 'a @user@example.com d'); - }); - - test('hashtag', () => { - assert.deepStrictEqual(fromHtml('

a #a d

', ['#a']), 'a #a d'); - }); -}); diff --git a/packages/backend/test/tests/reaction-lib.ts b/packages/backend/test/tests/reaction-lib.ts deleted file mode 100644 index 2e767f7697..0000000000 --- a/packages/backend/test/tests/reaction-lib.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* -import * as assert from 'assert'; - -import { toDbReaction } from '../src/misc/reaction-lib.js'; - -describe('toDbReaction', async () => { - test('既存の文字列リアクションはそのまま', async () => { - assert.strictEqual(await toDbReaction('like'), 'like'); - }); - - test('Unicodeプリンは寿司化不能とするため文字列化しない', async () => { - assert.strictEqual(await toDbReaction('🍮'), '🍮'); - }); - - test('プリン以外の既存のリアクションは文字列化する like', async () => { - assert.strictEqual(await toDbReaction('👍'), 'like'); - }); - - test('プリン以外の既存のリアクションは文字列化する love', async () => { - assert.strictEqual(await toDbReaction('❤️'), 'love'); - }); - - test('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => { - assert.strictEqual(await toDbReaction('❤'), 'love'); - }); - - test('プリン以外の既存のリアクションは文字列化する laugh', async () => { - assert.strictEqual(await toDbReaction('😆'), 'laugh'); - }); - - test('プリン以外の既存のリアクションは文字列化する hmm', async () => { - assert.strictEqual(await toDbReaction('🤔'), 'hmm'); - }); - - test('プリン以外の既存のリアクションは文字列化する surprise', async () => { - assert.strictEqual(await toDbReaction('😮'), 'surprise'); - }); - - test('プリン以外の既存のリアクションは文字列化する congrats', async () => { - assert.strictEqual(await toDbReaction('🎉'), 'congrats'); - }); - - test('プリン以外の既存のリアクションは文字列化する angry', async () => { - assert.strictEqual(await toDbReaction('💢'), 'angry'); - }); - - test('プリン以外の既存のリアクションは文字列化する confused', async () => { - assert.strictEqual(await toDbReaction('😥'), 'confused'); - }); - - test('プリン以外の既存のリアクションは文字列化する rip', async () => { - assert.strictEqual(await toDbReaction('😇'), 'rip'); - }); - - test('それ以外はUnicodeのまま', async () => { - assert.strictEqual(await toDbReaction('🍅'), '🍅'); - }); - - test('異体字セレクタ除去', async () => { - assert.strictEqual(await toDbReaction('㊗️'), '㊗'); - }); - - test('異体字セレクタ除去 必要なし', async () => { - assert.strictEqual(await toDbReaction('㊗'), '㊗'); - }); - - test('fallback - undefined', async () => { - assert.strictEqual(await toDbReaction(undefined), 'like'); - }); - - test('fallback - null', async () => { - assert.strictEqual(await toDbReaction(null), 'like'); - }); - - test('fallback - empty', async () => { - assert.strictEqual(await toDbReaction(''), 'like'); - }); - - test('fallback - unknown', async () => { - assert.strictEqual(await toDbReaction('unknown'), 'like'); - }); -}); -*/ diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index 5d91d0923a..8a024a678b 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -33,10 +33,12 @@ "lib": [ "esnext" ], - "types": ["jest"] + "types": ["jest", "node"] }, "compileOnSave": false, "include": [ - "./**/*.ts" + "./**/*.ts", + "../src/**/*.test.ts", + "../src/@types/**/*.ts", ] } diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts new file mode 100644 index 0000000000..5496738778 --- /dev/null +++ b/packages/backend/test/unit/MfmService.ts @@ -0,0 +1,102 @@ +import * as assert from 'assert'; +import * as mfm from 'mfm-js'; +import { Test } from '@nestjs/testing'; + +import { CoreModule } from '@/core/CoreModule.js'; +import { MfmService } from '@/core/MfmService.js'; +import { GlobalModule } from '@/GlobalModule.js'; + +describe('MfmService', () => { + let mfmService: MfmService; + + beforeAll(async () => { + const app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + }).compile(); + mfmService = app.get(MfmService); + }); + + describe('toHtml', () => { + test('br', () => { + const input = 'foo\nbar\nbaz'; + const output = '

foo
bar
baz

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

foo
bar
baz

'; + assert.equal(mfmService.toHtml(mfm.parse(input)), output); + }); + }); + + describe('fromHtml', () => { + test('p', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a

b

'), 'a\n\nb'); + }); + + test('block element', () => { + assert.deepStrictEqual(mfmService.fromHtml('
a
b
'), 'a\nb'); + }); + + test('inline element', () => { + assert.deepStrictEqual(mfmService.fromHtml('
  • a
  • b
'), 'a\nb'); + }); + + test('block code', () => { + assert.deepStrictEqual(mfmService.fromHtml('
a\nb
'), '```\na\nb\n```'); + }); + + test('inline code', () => { + assert.deepStrictEqual(mfmService.fromHtml('a'), '`a`'); + }); + + test('quote', () => { + assert.deepStrictEqual(mfmService.fromHtml('
a\nb
'), '> a\n> b'); + }); + + test('br', () => { + assert.deepStrictEqual(mfmService.fromHtml('

abc

d

'), 'abc\n\nd'); + }); + + test('link with different text', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a c d

'), 'a [c](https://example.com/b) d'); + }); + + test('link with different text, but not encoded', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a c d

'), 'a [c]() d'); + }); + + test('link with same text', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a https://example.com/b d

'), 'a https://example.com/b d'); + }); + + test('link with same text, but not encoded', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a https://example.com/ä d

'), 'a d'); + }); + + test('link with no url', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a c d

'), 'a [c](b) d'); + }); + + test('link without href', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a c d

'), 'a c d'); + }); + + test('link without text', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a d

'), 'a https://example.com/b d'); + }); + + test('link without both', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a d

'), 'a d'); + }); + + test('mention', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a @user d

'), 'a @user@example.com d'); + }); + + test('hashtag', () => { + assert.deepStrictEqual(mfmService.fromHtml('

a #a d

', ['#a']), 'a #a d'); + }); + }); +}); diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts new file mode 100644 index 0000000000..6a20a1e08e --- /dev/null +++ b/packages/backend/test/unit/ReactionService.ts @@ -0,0 +1,92 @@ +import * as assert from 'assert'; +import { Test } from '@nestjs/testing'; + +import { CoreModule } from '@/core/CoreModule.js'; +import { ReactionService } from '@/core/ReactionService.js'; +import { GlobalModule } from '@/GlobalModule.js'; + +describe('ReactionService', () => { + let reactionService: ReactionService; + + beforeAll(async () => { + const app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + }).compile(); + reactionService = app.get(ReactionService); + }); + + describe('toDbReaction', () => { + test('絵文字リアクションはそのまま', async () => { + assert.strictEqual(await reactionService.toDbReaction('👍'), '👍'); + assert.strictEqual(await reactionService.toDbReaction('🍅'), '🍅'); + }); + + test('既存のリアクションは絵文字化する pudding', async () => { + assert.strictEqual(await reactionService.toDbReaction('pudding'), '🍮'); + }); + + test('既存のリアクションは絵文字化する like', async () => { + assert.strictEqual(await reactionService.toDbReaction('like'), '👍'); + }); + + test('既存のリアクションは絵文字化する love', async () => { + assert.strictEqual(await reactionService.toDbReaction('love'), '❤'); + }); + + test('既存のリアクションは絵文字化する laugh', async () => { + assert.strictEqual(await reactionService.toDbReaction('laugh'), '😆'); + }); + + test('既存のリアクションは絵文字化する hmm', async () => { + assert.strictEqual(await reactionService.toDbReaction('hmm'), '🤔'); + }); + + test('既存のリアクションは絵文字化する surprise', async () => { + assert.strictEqual(await reactionService.toDbReaction('surprise'), '😮'); + }); + + test('既存のリアクションは絵文字化する congrats', async () => { + assert.strictEqual(await reactionService.toDbReaction('congrats'), '🎉'); + }); + + test('既存のリアクションは絵文字化する angry', async () => { + assert.strictEqual(await reactionService.toDbReaction('angry'), '💢'); + }); + + test('既存のリアクションは絵文字化する confused', async () => { + assert.strictEqual(await reactionService.toDbReaction('confused'), '😥'); + }); + + test('既存のリアクションは絵文字化する rip', async () => { + assert.strictEqual(await reactionService.toDbReaction('rip'), '😇'); + }); + + test('既存のリアクションは絵文字化する star', async () => { + assert.strictEqual(await reactionService.toDbReaction('star'), '⭐'); + }); + + test('異体字セレクタ除去', async () => { + assert.strictEqual(await reactionService.toDbReaction('㊗️'), '㊗'); + }); + + test('異体字セレクタ除去 必要なし', async () => { + assert.strictEqual(await reactionService.toDbReaction('㊗'), '㊗'); + }); + + test('fallback - undefined', async () => { + assert.strictEqual(await reactionService.toDbReaction(undefined), '👍'); + }); + + test('fallback - null', async () => { + assert.strictEqual(await reactionService.toDbReaction(null), '👍'); + }); + + test('fallback - empty', async () => { + assert.strictEqual(await reactionService.toDbReaction(''), '👍'); + }); + + test('fallback - unknown', async () => { + assert.strictEqual(await reactionService.toDbReaction('unknown'), '👍'); + }); + }); +}); diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 221f743d3a..6fe04274e6 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -3,16 +3,18 @@ process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; -import { DataSource } from 'typeorm'; +import * as lolex from '@sinonjs/fake-timers'; import rndstr from 'rndstr'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { CoreModule } from '@/core/CoreModule.js'; import { MetaService } from '@/core/MetaService.js'; import { genAid } from '@/misc/id/aid.js'; import { UserCacheService } from '@/core/UserCacheService.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { sleep } from '../utils.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -25,6 +27,7 @@ describe('RoleService', () => { let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; let metaService: jest.Mocked; + let clock: lolex.InstalledClock; function createUser(data: Partial = {}) { const un = rndstr('a-z0-9', 16); @@ -50,16 +53,12 @@ describe('RoleService', () => { .then(x => rolesRepository.findOneByOrFail(x.identifiers[0])); } - async function assign(roleId: Role['id'], userId: User['id']) { - await roleAssignmentsRepository.insert({ - id: genAid(new Date()), - createdAt: new Date(), - roleId, - userId, - }); - } - beforeEach(async () => { + clock = lolex.install({ + now: new Date(), + shouldClearNativeTimers: true, + }); + app = await Test.createTestingModule({ imports: [ GlobalModule, @@ -67,6 +66,8 @@ describe('RoleService', () => { providers: [ RoleService, UserCacheService, + IdService, + GlobalEventService, ], }) .useMocker((token) => { @@ -92,12 +93,15 @@ describe('RoleService', () => { }); afterEach(async () => { + clock.uninstall(); + await Promise.all([ app.get(DI.metasRepository).delete({}), usersRepository.delete({}), rolesRepository.delete({}), roleAssignmentsRepository.delete({}), ]); + await app.close(); }); @@ -115,7 +119,7 @@ describe('RoleService', () => { expect(result.canManageCustomEmojis).toBe(false); }); - test('instance default policies 2', async () => { + test('instance default policies 2', async () => { const user = await createUser(); metaService.fetch.mockResolvedValue({ policies: { @@ -128,7 +132,7 @@ describe('RoleService', () => { expect(result.canManageCustomEmojis).toBe(true); }); - test('with role', async () => { + test('with role', async () => { const user = await createUser(); const role = await createRole({ name: 'a', @@ -140,7 +144,7 @@ describe('RoleService', () => { }, }, }); - await assign(role.id, user.id); + await roleService.assign(user.id, role.id); metaService.fetch.mockResolvedValue({ policies: { canManageCustomEmojis: false, @@ -152,7 +156,7 @@ describe('RoleService', () => { expect(result.canManageCustomEmojis).toBe(true); }); - test('priority', async () => { + test('priority', async () => { const user = await createUser(); const role1 = await createRole({ name: 'role1', @@ -174,8 +178,8 @@ describe('RoleService', () => { }, }, }); - await assign(role1.id, user.id); - await assign(role2.id, user.id); + await roleService.assign(user.id, role1.id); + await roleService.assign(user.id, role2.id); metaService.fetch.mockResolvedValue({ policies: { driveCapacityMb: 50, @@ -187,7 +191,7 @@ describe('RoleService', () => { expect(result.driveCapacityMb).toBe(100); }); - test('conditional role', async () => { + test('conditional role', async () => { const user1 = await createUser({ createdAt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 365)), }); @@ -228,5 +232,42 @@ describe('RoleService', () => { expect(user1Policies.canManageCustomEmojis).toBe(false); expect(user2Policies.canManageCustomEmojis).toBe(true); }); + + test('expired role', async () => { + const user = await createUser(); + const role = await createRole({ + name: 'a', + policies: { + canManageCustomEmojis: { + useDefault: false, + priority: 0, + value: true, + }, + }, + }); + await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24))); + metaService.fetch.mockResolvedValue({ + policies: { + canManageCustomEmojis: false, + }, + } as any); + + const result = await roleService.getUserPolicies(user.id); + expect(result.canManageCustomEmojis).toBe(true); + + clock.tick('25:00:00'); + + const resultAfter25h = await roleService.getUserPolicies(user.id); + expect(resultAfter25h.canManageCustomEmojis).toBe(false); + + await roleService.assign(user.id, role.id); + + // ストリーミング経由で反映されるまでちょっと待つ + clock.uninstall(); + await sleep(100); + + const resultAfter25hAgain = await roleService.getUserPolicies(user.id); + expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); + }); }); }); diff --git a/packages/backend/test/tests/activitypub.ts b/packages/backend/test/unit/activitypub.ts similarity index 56% rename from packages/backend/test/tests/activitypub.ts rename to packages/backend/test/unit/activitypub.ts index 19fb5d90d7..3d0032507e 100644 --- a/packages/backend/test/tests/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -2,8 +2,39 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import rndstr from 'rndstr'; +import { Test } from '@nestjs/testing'; +import { jest } from '@jest/globals'; + +import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; +import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import { MockResolver } from '../misc/mock-resolver.js'; describe('ActivityPub', () => { + let noteService: ApNoteService; + let personService: ApPersonService; + let resolver: MockResolver; + + beforeEach(async () => { + const app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + }).compile(); + + await app.init(); + app.enableShutdownHooks(); + + noteService = app.get(ApNoteService); + personService = app.get(ApPersonService); + resolver = new MockResolver(await app.resolve(LoggerService)); + + // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error + const federatedInstanceService = app.get(FederatedInstanceService); + jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => {})); + }); + describe('Parse minimum object', () => { const host = 'https://host1.test'; const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; @@ -28,13 +59,9 @@ describe('ActivityPub', () => { }; test('Minimum Actor', async () => { - const { MockResolver } = await import('../misc/mock-resolver.js'); - const { createPerson } = await import('../../src/activitypub/models/person.js'); - - const resolver = new MockResolver(); resolver._register(actor.id, actor); - const user = await createPerson(actor.id, resolver); + const user = await personService.createPerson(actor.id, resolver); assert.deepStrictEqual(user.uri, actor.id); assert.deepStrictEqual(user.username, actor.preferredUsername); @@ -42,14 +69,10 @@ describe('ActivityPub', () => { }); test('Minimum Note', async () => { - const { MockResolver } = await import('../misc/mock-resolver.js'); - const { createNote } = await import('../../src/activitypub/models/note.js'); - - const resolver = new MockResolver(); resolver._register(actor.id, actor); resolver._register(post.id, post); - const note = await createNote(post.id, resolver, true); + const note = await noteService.createNote(post.id, resolver, true); assert.deepStrictEqual(note?.uri, post.id); assert.deepStrictEqual(note.visibility, 'public'); @@ -75,13 +98,9 @@ describe('ActivityPub', () => { }; test('Actor', async () => { - const { MockResolver } = await import('../misc/mock-resolver.js'); - const { createPerson } = await import('../../src/activitypub/models/person.js'); - - const resolver = new MockResolver(); resolver._register(actor.id, actor); - const user = await createPerson(actor.id, resolver); + const user = await personService.createPerson(actor.id, resolver); assert.deepStrictEqual(user.name, actor.name.substr(0, 128)); }); diff --git a/packages/backend/test/tests/ap-request.ts b/packages/backend/test/unit/ap-request.ts similarity index 78% rename from packages/backend/test/tests/ap-request.ts rename to packages/backend/test/unit/ap-request.ts index 8c586861ad..98f352e1c6 100644 --- a/packages/backend/test/tests/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -1,7 +1,8 @@ import * as assert from 'assert'; import httpSignature from '@peertube/http-signature'; -import { genRsaKeyPair } from '../../src/misc/gen-key-pair.js'; -import { createSignedPost, createSignedGet } from '../../src/activitypub/ap-request.js'; + +import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; +import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { return { @@ -9,7 +10,7 @@ export const buildParsedSignature = (signingString: string, signature: string, a params: { keyId: 'KeyID', // dummy, not used for verify algorithm: algorithm, - headers: [ '(request-target)', 'date', 'host', 'digest' ], // dummy, not used for verify + headers: ['(request-target)', 'date', 'host', 'digest'], // dummy, not used for verify signature: signature, }, signingString: signingString, @@ -29,7 +30,7 @@ describe('ap-request', () => { 'User-Agent': 'UA', }; - const req = createSignedPost({ key, url, body, additionalHeaders: headers }); + const req = ApRequestCreator.createSignedPost({ key, url, body, additionalHeaders: headers }); const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); @@ -45,7 +46,7 @@ describe('ap-request', () => { 'User-Agent': 'UA', }; - const req = createSignedGet({ key, url, additionalHeaders: headers }); + const req = ApRequestCreator.createSignedGet({ key, url, additionalHeaders: headers }); const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts index 1e9a51bc88..5ac4cc18a2 100644 --- a/packages/backend/test/unit/chart.ts +++ b/packages/backend/test/unit/chart.ts @@ -19,7 +19,7 @@ import Logger from '@/logger.js'; describe('Chart', () => { const config = loadConfig(); const appLockService = { - getChartInsertLock: jest.fn().mockImplementation(() => Promise.resolve(() => {})), + getChartInsertLock: () => () => Promise.resolve(() => {}), } as unknown as jest.Mocked; let db: DataSource | undefined; diff --git a/packages/backend/test/tests/extract-mentions.ts b/packages/backend/test/unit/extract-mentions.ts similarity index 81% rename from packages/backend/test/tests/extract-mentions.ts rename to packages/backend/test/unit/extract-mentions.ts index e81d04c2db..66d32be1c5 100644 --- a/packages/backend/test/tests/extract-mentions.ts +++ b/packages/backend/test/unit/extract-mentions.ts @@ -1,11 +1,11 @@ import * as assert from 'assert'; import { parse } from 'mfm-js'; -import { extractMentions } from '../../src/misc/extract-mentions.js'; +import { extractMentions } from '@/misc/extract-mentions.js'; describe('Extract mentions', () => { test('simple', () => { - const ast = parse('@foo @bar @baz')!; + const ast = parse('@foo @bar @baz'); const mentions = extractMentions(ast); assert.deepStrictEqual(mentions, [{ username: 'foo', @@ -23,7 +23,7 @@ describe('Extract mentions', () => { }); test('nested', () => { - const ast = parse('@foo **@bar** @baz')!; + const ast = parse('@foo **@bar** @baz'); const mentions = extractMentions(ast); assert.deepStrictEqual(mentions, [{ username: 'foo', diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 50988939aa..8203e49359 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,87 +1,50 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import * as childProcess from 'child_process'; -import * as http from 'node:http'; -import { SIGKILL } from 'constants'; +import { readFile } from 'node:fs/promises'; +import { isAbsolute, basename } from 'node:path'; import WebSocket from 'ws'; -import fetch from 'node-fetch'; -import FormData from 'form-data'; +import fetch, { Blob, File, RequestInit } from 'node-fetch'; import { DataSource } from 'typeorm'; -import got, { RequestError } from 'got'; -import loadConfig from '../src/config/load.js'; -import { entities } from '@/postgres.js'; +import { entities } from '../src/postgres.js'; +import { loadConfig } from '../src/config.js'; import type * as misskey from 'misskey-js'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); +export { server as startServer } from '@/boot/common.js'; const config = loadConfig(); export const port = config.port; export const api = async (endpoint: string, params: any, me?: any) => { - endpoint = endpoint.replace(/^\//, ''); - - const auth = me ? { - i: me.token, - } : {}; - - try { - const res = await got(`http://localhost:${port}/api/${endpoint}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(Object.assign(auth, params)), - retry: { - limit: 0, - }, - }); - - const status = res.statusCode; - const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; - - return { - status, - body, - }; - } catch (err: unknown) { - if (err instanceof RequestError && err.response) { - const status = err.response.statusCode; - const body = await JSON.parse(err.response.body as string); - - return { - status, - body, - }; - } else { - throw err; - } - } + const normalized = endpoint.replace(/^\//, ''); + return await request(`api/${normalized}`, params, me); }; -export const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { +const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { const auth = me ? { i: me.token, } : {}; - const res = await fetch(`http://localhost:${port}/${path}`, { + const res = await relativeFetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(Object.assign(auth, params)), + redirect: 'manual', }); const status = res.status; - const body = res.status === 200 ? await res.json().catch() : null; + const body = res.headers.get('content-type') === 'application/json; charset=utf-8' + ? await res.json() + : null; return { body, status, }; }; +const relativeFetch = async (path: string, init?: RequestInit | undefined) => { + return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init); +}; + export const signup = async (params?: any): Promise => { const q = Object.assign({ username: 'test', @@ -110,30 +73,46 @@ export const react = async (user: any, note: any, reaction: string): Promise => { - const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`; +export const uploadFile = async (user: any, { path, name, blob }: UploadOptions = {}): Promise => { + const absPath = path == null + ? new URL('resources/Lenna.jpg', import.meta.url) + : isAbsolute(path.toString()) + ? new URL(path) + : new URL(path, new URL('resources/', import.meta.url)); - const formData = new FormData() as any; + const formData = new FormData(); formData.append('i', user.token); - formData.append('file', fs.createReadStream(absPath)); + formData.append('file', blob ?? + new File([await readFile(absPath)], basename(absPath.toString()))); formData.append('force', 'true'); + if (name) { + formData.append('name', name); + } - const res = await got(`http://localhost:${port}/api/drive/files/create`, { + const res = await relativeFetch('api/drive/files/create', { method: 'POST', body: formData, - retry: { - limit: 0, - }, }); - const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; + const body = res.status !== 204 ? await res.json() : null; - return body; + return { + status: res.status, + body, + }; }; export const uploadUrl = async (user: any, url: string) => { @@ -160,7 +139,7 @@ export const uploadUrl = async (user: any, url: string) => { export function connectStream(user: any, channel: string, listener: (message: Record) => any, params?: any): Promise { return new Promise((res, rej) => { - const ws = new WebSocket(`ws://localhost:${port}/streaming?i=${user.token}`); + const ws = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${user.token}`); ws.on('open', () => { ws.on('message', data => { @@ -187,7 +166,7 @@ export function connectStream(user: any, channel: string, listener: (message: Re export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { return new Promise(async (res, rej) => { - let timer: NodeJS.Timeout; + let timer: NodeJS.Timeout | null = null; let ws: WebSocket; try { @@ -219,41 +198,25 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond }); }; -export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => { - // node-fetchだと3xxを取れない - return await new Promise((resolve, reject) => { - const req = http.request(`http://localhost:${port}${path}`, { - headers: { - Accept: accept, - }, - }, res => { - if (res.statusCode! >= 400) { - reject(res); - } else { - resolve({ - status: res.statusCode, - type: res.headers['content-type'], - location: res.headers.location, - }); - } - }); - - req.end(); +export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status: number, body: any, type: string | null, location: string | null }> => { + const res = await relativeFetch(path, { + headers: { + Accept: accept, + }, + redirect: 'manual', }); -}; -export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise = async () => {}) { - return (done: (err?: Error) => any) => { - const p = childProcess.spawn('node', [_dirname + '/../index.js'], { - stdio: ['inherit', 'inherit', 'inherit', 'ipc'], - env: { NODE_ENV: 'test', PATH: process.env.PATH }, - }); - callbackSpawnedProcess(p); - p.on('message', message => { - if (message === 'ok') moreProcess().then(() => done()).catch(e => done(e)); - }); + const body = res.headers.get('content-type') === 'application/json; charset=utf-8' + ? await res.json() + : null; + + return { + status: res.status, + body, + type: res.headers.get('content-type'), + location: res.headers.get('location'), }; -} +}; export async function initTestDb(justBorrow = false, initEntities?: any[]) { if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; @@ -275,46 +238,6 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { return db; } -export function startServer(timeout = 60 * 1000): Promise { - return new Promise((res, rej) => { - const t = setTimeout(() => { - p.kill(SIGKILL); - rej('timeout to start'); - }, timeout); - - const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], { - stdio: ['inherit', 'inherit', 'inherit', 'ipc'], - env: { NODE_ENV: 'test', PATH: process.env.PATH }, - }); - - p.on('error', e => rej(e)); - - p.on('message', message => { - if (message === 'ok') { - clearTimeout(t); - res(p); - } - }); - }); -} - -export function shutdownServer(p: childProcess.ChildProcess | undefined, timeout = 20 * 1000) { - if (p == null) return Promise.resolve('nop'); - return new Promise((res, rej) => { - const t = setTimeout(() => { - p.kill(SIGKILL); - res('force exit'); - }, timeout); - - p.once('exit', () => { - clearTimeout(t); - res('exited'); - }); - - p.kill(); - }); -} - export function sleep(msec: number) { return new Promise(res => { setTimeout(() => { diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 544b529e94..faadbcdfc6 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "allowJs": true, - "noEmitOnError": false, + "noEmitOnError": true, "noImplicitAny": true, "noImplicitReturns": true, "noUnusedParameters": false, @@ -26,9 +26,7 @@ "rootDir": "./src", "baseUrl": "./", "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] }, "outDir": "./built", "types": [ @@ -46,4 +44,7 @@ "include": [ "./src/**/*.ts" ], + "exclude": [ + "./src/**/*.test.ts" + ] } diff --git a/packages/frontend/.eslintrc.js b/packages/frontend/.eslintrc.js index 6c3bfb5a6e..e8e0e57d2a 100644 --- a/packages/frontend/.eslintrc.js +++ b/packages/frontend/.eslintrc.js @@ -55,6 +55,7 @@ module.exports = { 'vue/multi-word-component-names': 'warn', 'vue/require-v-for-key': 'warn', 'vue/no-unused-components': 'warn', + 'vue/no-unused-vars': 'warn', 'vue/valid-v-for': 'warn', 'vue/return-in-computed-property': 'warn', 'vue/no-setup-props-destructure': 'warn', diff --git a/packages/frontend/assets/about-icon.png b/packages/frontend/assets/about-icon.png index afc1f0c728..15fd1e3731 100644 Binary files a/packages/frontend/assets/about-icon.png and b/packages/frontend/assets/about-icon.png differ diff --git a/packages/frontend/assets/label-red.svg b/packages/frontend/assets/label-red.svg index 45996aa9ce..c89d3f5f3a 100644 Binary files a/packages/frontend/assets/label-red.svg and b/packages/frontend/assets/label-red.svg differ diff --git a/packages/frontend/assets/label.svg b/packages/frontend/assets/label.svg index b1f85f3c07..997335f505 100644 Binary files a/packages/frontend/assets/label.svg and b/packages/frontend/assets/label.svg differ diff --git a/packages/frontend/assets/sounds/syuilo/n-aec-4va.mp3 b/packages/frontend/assets/sounds/syuilo/n-aec-4va.mp3 new file mode 100644 index 0000000000..2ef7024beb Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-aec-4va.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-aec-4vb.mp3 b/packages/frontend/assets/sounds/syuilo/n-aec-4vb.mp3 new file mode 100644 index 0000000000..a8ad11287e Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-aec-4vb.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-aec-8va.mp3 b/packages/frontend/assets/sounds/syuilo/n-aec-8va.mp3 new file mode 100644 index 0000000000..8cc2ead028 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-aec-8va.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-aec-8vb.mp3 b/packages/frontend/assets/sounds/syuilo/n-aec-8vb.mp3 new file mode 100644 index 0000000000..59cbcaf917 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-aec-8vb.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-aec.mp3 b/packages/frontend/assets/sounds/syuilo/n-aec.mp3 new file mode 100644 index 0000000000..7aec0516e7 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-aec.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-cea-4va.mp3 b/packages/frontend/assets/sounds/syuilo/n-cea-4va.mp3 new file mode 100644 index 0000000000..98fe354d67 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-cea-4va.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-cea-4vb.mp3 b/packages/frontend/assets/sounds/syuilo/n-cea-4vb.mp3 new file mode 100644 index 0000000000..7b69b3410d Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-cea-4vb.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-cea-8va.mp3 b/packages/frontend/assets/sounds/syuilo/n-cea-8va.mp3 new file mode 100644 index 0000000000..44f2deee3d Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-cea-8va.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-cea-8vb.mp3 b/packages/frontend/assets/sounds/syuilo/n-cea-8vb.mp3 new file mode 100644 index 0000000000..1342a56f85 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-cea-8vb.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-cea.mp3 b/packages/frontend/assets/sounds/syuilo/n-cea.mp3 new file mode 100644 index 0000000000..88d641fd64 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-cea.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-ea-4va.mp3 b/packages/frontend/assets/sounds/syuilo/n-ea-4va.mp3 new file mode 100644 index 0000000000..468b82bc2c Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-ea-4va.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-ea-4vb.mp3 b/packages/frontend/assets/sounds/syuilo/n-ea-4vb.mp3 new file mode 100644 index 0000000000..3869e894d2 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-ea-4vb.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-ea-8va.mp3 b/packages/frontend/assets/sounds/syuilo/n-ea-8va.mp3 new file mode 100644 index 0000000000..f268b7ee8b Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-ea-8va.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-ea-8vb.mp3 b/packages/frontend/assets/sounds/syuilo/n-ea-8vb.mp3 new file mode 100644 index 0000000000..d6e895e67b Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-ea-8vb.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-ea-harmony.mp3 b/packages/frontend/assets/sounds/syuilo/n-ea-harmony.mp3 new file mode 100644 index 0000000000..8e055db91d Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-ea-harmony.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-ea.mp3 b/packages/frontend/assets/sounds/syuilo/n-ea.mp3 new file mode 100644 index 0000000000..c13d13247b Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-ea.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-eca-4va.mp3 b/packages/frontend/assets/sounds/syuilo/n-eca-4va.mp3 new file mode 100644 index 0000000000..06577d5431 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-eca-4va.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-eca-4vb.mp3 b/packages/frontend/assets/sounds/syuilo/n-eca-4vb.mp3 new file mode 100644 index 0000000000..1622033d6a Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-eca-4vb.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-eca-8va.mp3 b/packages/frontend/assets/sounds/syuilo/n-eca-8va.mp3 new file mode 100644 index 0000000000..a3a9f3a9fb Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-eca-8va.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-eca-8vb.mp3 b/packages/frontend/assets/sounds/syuilo/n-eca-8vb.mp3 new file mode 100644 index 0000000000..4efa848407 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-eca-8vb.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/n-eca.mp3 b/packages/frontend/assets/sounds/syuilo/n-eca.mp3 new file mode 100644 index 0000000000..1a3979cc61 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/n-eca.mp3 differ diff --git a/packages/frontend/assets/unread.svg b/packages/frontend/assets/unread.svg index 8c3cc9f475..8bd4156e51 100644 Binary files a/packages/frontend/assets/unread.svg and b/packages/frontend/assets/unread.svg differ diff --git a/packages/frontend/package.json b/packages/frontend/package.json index bf22f7aaad..e4c04f5937 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,7 +4,9 @@ "scripts": { "watch": "vite", "build": "vite build", - "lint": "vue-tsc --noEmit && eslint --quiet \"src/**/*.{ts,vue}\"" + "typecheck": "vue-tsc --noEmit", + "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", + "lint": "pnpm typecheck && pnpm eslint" }, "dependencies": { "@discordapp/twemoji": "14.0.2", @@ -17,11 +19,11 @@ "@vue/compiler-sfc": "3.2.47", "autobind-decorator": "2.4.0", "autosize": "5.0.2", - "blurhash": "2.0.4", + "blurhash": "2.0.5", "broadcast-channel": "4.20.2", - "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", + "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "canvas-confetti": "1.6.0", - "chart.js": "4.2.0", + "chart.js": "4.2.1", "chartjs-adapter-date-fns": "3.0.0", "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", @@ -36,24 +38,23 @@ "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", - "matter-js": "0.18.0", + "matter-js": "0.19.0", "mfm-js": "0.23.3", "misskey-js": "0.0.15", - "photoswipe": "5.3.5", + "photoswipe": "5.3.6", "prismjs": "1.29.0", "punycode": "2.3.0", "querystring": "0.2.1", "rndstr": "1.0.0", - "rollup": "3.14.0", + "rollup": "3.17.3", "s-age": "1.1.2", - "sanitize-html": "2.9.0", - "sass": "1.58.0", + "sanitize-html": "2.10.0", + "sass": "1.58.3", "seedrandom": "3.0.5", "strict-event-emitter-types": "2.0.0", - "stringz": "2.1.0", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", - "three": "0.149.0", + "three": "0.150.0", "throttle-debounce": "5.0.0", "tinycolor2": "1.6.0", "tsc-alias": "1.8.2", @@ -62,37 +63,36 @@ "typescript": "4.9.5", "uuid": "9.0.0", "vanilla-tilt": "1.8.0", - "vue-plyr": "7.0.0", - "vite": "4.1.1", + "vite": "4.1.4", "vue": "3.2.47", + "vue-plyr": "7.0.0", "vue-prism-editor": "2.0.0-alpha.2", "vuedraggable": "next" }, "devDependencies": { "@types/escape-regexp": "0.0.1", - "@types/glob": "8.0.1", "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", "@types/matter-js": "0.18.2", - "@types/node": "18.13.0", + "@types/node": "18.14.1", "@types/punycode": "2.1.0", "@types/sanitize-html": "2.8.0", - "@types/seedrandom": "3.0.4", + "@types/seedrandom": "3.0.5", "@types/throttle-debounce": "5.0.0", "@types/tinycolor2": "1.4.3", - "@types/uuid": "9.0.0", + "@types/uuid": "9.0.1", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.51.0", - "@typescript-eslint/parser": "5.51.0", + "@typescript-eslint/eslint-plugin": "5.53.0", + "@typescript-eslint/parser": "5.53.0", "@vue/runtime-core": "3.2.47", "cross-env": "7.0.3", - "cypress": "12.5.1", - "eslint": "8.33.0", + "cypress": "12.7.0", + "eslint": "8.35.0", "eslint-plugin-import": "2.27.5", "eslint-plugin-vue": "9.9.0", - "start-server-and-test": "1.15.3", + "start-server-and-test": "1.15.4", "vue-eslint-parser": "9.1.0", - "vue-tsc": "1.0.24" + "vue-tsc": "1.2.0" } } diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index 0e18a5a83d..dee80378e6 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -39,7 +39,6 @@ import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; -import { acct, userPage } from '@/filters/user'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { dateString } from '@/filters/date'; diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue index a76a1e0f54..9f2bf99338 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.vue +++ b/packages/frontend/src/components/MkAbuseReportWindow.vue @@ -43,7 +43,7 @@ const emit = defineEmits<{ }>(); const uiWindow = shallowRef>(); -const comment = ref(props.initialComment || ''); +const comment = ref(props.initialComment ?? ''); function send() { os.apiWithDialog('users/report-abuse', { diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index 19d04721d8..d30037dcf9 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -107,6 +107,7 @@ onMounted(() => { } .iconFrame { + position: relative; width: 58px; height: 58px; padding: 6px; diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index 139e49cc40..1218202616 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -73,7 +73,7 @@ diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index af7175e5cd..bfec57d6a0 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -88,7 +88,7 @@ - diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index eee77a9475..9e3022896c 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -36,7 +36,7 @@
- + + diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index b7727ca30d..4f8afb9ea2 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -1,93 +1,106 @@ - - diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index df8417e4ad..38c5b1e082 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -27,7 +27,7 @@ + diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index 3a74e8518d..c441407d97 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -7,20 +7,20 @@
@@ -29,28 +29,28 @@
- {{ tag.tag }} - {{ tag.tag }} + {{ tag.tag }} + {{ tag.tag }}
- +
@@ -58,14 +58,12 @@ - - diff --git a/packages/frontend/src/pages/messaging/messaging-room.form.vue b/packages/frontend/src/pages/messaging/messaging-room.form.vue deleted file mode 100644 index d6113668dd..0000000000 --- a/packages/frontend/src/pages/messaging/messaging-room.form.vue +++ /dev/null @@ -1,366 +0,0 @@ - - - - - diff --git a/packages/frontend/src/pages/messaging/messaging-room.message.vue b/packages/frontend/src/pages/messaging/messaging-room.message.vue deleted file mode 100644 index d10798b92e..0000000000 --- a/packages/frontend/src/pages/messaging/messaging-room.message.vue +++ /dev/null @@ -1,338 +0,0 @@ - - - - - diff --git a/packages/frontend/src/pages/messaging/messaging-room.vue b/packages/frontend/src/pages/messaging/messaging-room.vue deleted file mode 100644 index 0867f003a3..0000000000 --- a/packages/frontend/src/pages/messaging/messaging-room.vue +++ /dev/null @@ -1,415 +0,0 @@ - - - - - diff --git a/packages/frontend/src/pages/mfm-cheat-sheet.vue b/packages/frontend/src/pages/mfm-cheat-sheet.vue deleted file mode 100644 index 73a5716236..0000000000 --- a/packages/frontend/src/pages/mfm-cheat-sheet.vue +++ /dev/null @@ -1,377 +0,0 @@ - - - - - diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 9a4019e5b1..915adff277 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -1,41 +1,40 @@ - diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index 005b036696..c35af3e22a 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -5,7 +5,6 @@ - diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 86b3fce3c5..165e357ebd 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -6,7 +6,7 @@
- +
@@ -29,10 +29,10 @@
- +
- +
@@ -41,11 +41,10 @@ + diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index 6075dde326..fb78546cb1 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -44,7 +44,7 @@ import * as os from '@/os'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; -import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui'; +import { AsUiComponent, AsUiRoot, registerAsUiLib } from '@/scripts/aiscript/ui'; import MkAsUi from '@/components/MkAsUi.vue'; import { miLocalStorage } from '@/local-storage'; import { claimAchievement } from '@/scripts/achievements'; diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue index 7918f9f577..7e81cd2c0d 100644 --- a/packages/frontend/src/pages/search.vue +++ b/packages/frontend/src/pages/search.vue @@ -2,33 +2,104 @@ - + + + + + + + + +
+ +
+
+ + + + + + + +
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue new file mode 100644 index 0000000000..1d836db5f5 --- /dev/null +++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index e6ef09668c..891934d706 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -1,216 +1,258 @@ diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue index 3f1f2820f0..bc0179b3aa 100644 --- a/packages/frontend/src/pages/settings/deck.vue +++ b/packages/frontend/src/pages/settings/deck.vue @@ -13,14 +13,10 @@ diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 5692ce80cb..c83c48d5ad 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -46,7 +46,7 @@ - {{ i18n.ts._visibility.localOnly }} + {{ i18n.ts._visibility.disableFederation }} diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index da7d3d3703..41563c441f 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -78,7 +78,6 @@ import MkSelect from '@/components/MkSelect.vue'; import FormSplit from '@/components/form/split.vue'; import MkFolder from '@/components/MkFolder.vue'; import FormSlot from '@/components/form/slot.vue'; -import { host } from '@/config'; import { selectFile } from '@/scripts/select-file'; import * as os from '@/os'; import { i18n } from '@/i18n'; @@ -125,11 +124,11 @@ function saveFields() { function save() { os.apiWithDialog('i/update', { - name: profile.name || null, - description: profile.description || null, - location: profile.location || null, - birthday: profile.birthday || null, - lang: profile.lang || null, + name: profile.name ?? null, + description: profile.description ?? null, + location: profile.location ?? null, + birthday: profile.birthday ?? null, + lang: profile.lang ?? null, isBot: !!profile.isBot, isCat: !!profile.isCat, showTimelineReplies: !!profile.showTimelineReplies, @@ -150,6 +149,8 @@ function changeAvatar(ev) { const { canceled } = await os.confirm({ type: 'question', text: i18n.t('cropImageAsk'), + okText: i18n.ts.cropYes, + cancelText: i18n.ts.cropNo, }); if (!canceled) { @@ -174,6 +175,8 @@ function changeBanner(ev) { const { canceled } = await os.confirm({ type: 'question', text: i18n.t('cropImageAsk'), + okText: i18n.ts.cropYes, + cancelText: i18n.ts.cropNo, }); if (!canceled) { diff --git a/packages/frontend/src/pages/settings/reaction.vue b/packages/frontend/src/pages/settings/reaction.vue index c8b47b8299..ed913731d3 100644 --- a/packages/frontend/src/pages/settings/reaction.vue +++ b/packages/frontend/src/pages/settings/reaction.vue @@ -57,7 +57,6 @@ diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue index 01c31688cc..e10f65b0af 100644 --- a/packages/frontend/src/pages/settings/webhook.vue +++ b/packages/frontend/src/pages/settings/webhook.vue @@ -30,9 +30,6 @@ import { } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; -import { userPage } from '@/filters/user'; -import * as os from '@/os'; -import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; const pagination = { diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index d8b956b6d1..78e0710162 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -2,7 +2,7 @@ - (); +let reply = $ref(); +let renote = $ref(); +let visibility = $ref(noteVisibilities.includes(visibilityQuery) ? visibilityQuery : undefined); +let localOnly = $ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined); let files = $ref([] as Misskey.entities.DriveFile[]); let visibleUsers = $ref([] as Misskey.entities.User[]); @@ -130,7 +130,7 @@ async function init() { ); } //#endregion - } catch (err) { + } catch (err: any) { os.alert({ type: 'error', title: err.message, diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index 5d6d01d2ae..511052c424 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -2,14 +2,14 @@ - + + diff --git a/packages/frontend/src/pages/user/achievements.vue b/packages/frontend/src/pages/user/achievements.vue index 615613b7fc..1b3a6e24b3 100644 --- a/packages/frontend/src/pages/user/achievements.vue +++ b/packages/frontend/src/pages/user/achievements.vue @@ -5,10 +5,9 @@ diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts index 2ca89b7351..2616a8a1d5 100644 --- a/packages/frontend/src/pizzax.ts +++ b/packages/frontend/src/pizzax.ts @@ -48,8 +48,8 @@ export class Storage { // 簡易的にキューイングして占有ロックとする private currentIdbJob: Promise = Promise.resolve(); private addIdbSetJob(job: () => Promise) { - const promise = this.currentIdbJob.then(job, e => { - console.error('Pizzax failed to save data to idb!', e); + const promise = this.currentIdbJob.then(job, err => { + console.error('Pizzax failed to save data to idb!', err); return job(); }); this.currentIdbJob = promise; @@ -130,22 +130,22 @@ export class Storage { await defaultStore.ready; api('i/registry/get-all', { scope: ['client', this.key] }) - .then(kvs => { - const cache: Partial = {}; - for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) { - if (v.where === 'account') { - if (Object.prototype.hasOwnProperty.call(kvs, k)) { - this.reactiveState[k].value = this.state[k] = (kvs as Partial)[k]; - cache[k] = (kvs as Partial)[k]; - } else { - this.reactiveState[k].value = this.state[k] = v.default; + .then(kvs => { + const cache: Partial = {}; + for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) { + if (v.where === 'account') { + if (Object.prototype.hasOwnProperty.call(kvs, k)) { + this.reactiveState[k].value = this.state[k] = (kvs as Partial)[k]; + cache[k] = (kvs as Partial)[k]; + } else { + this.reactiveState[k].value = this.state[k] = v.default; + } } } - } - return set(this.registryCacheKeyName, cache); - }) - .then(() => resolve()); + return set(this.registryCacheKeyName, cache); + }) + .then(() => resolve()); }, 1); } else { resolve(); diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index 17eb99be22..a1a36480fd 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -1,12 +1,12 @@ import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; import { createAiScriptEnv } from '@/scripts/aiscript/api'; import { inputText } from '@/os'; -import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@/store'; +import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@/store'; const parser = new Parser(); const pluginContexts = new Map(); -export function install(plugin) { +export function install(plugin: Plugin): void { // 後方互換性のため if (plugin.src == null) return; console.info('Plugin installed:', plugin.name, 'v' + plugin.version); @@ -15,7 +15,7 @@ export function install(plugin) { plugin: plugin, storageKey: 'plugins:' + plugin.id, }), { - in: (q) => { + in: (q): Promise => { return new Promise(ok => { inputText({ title: q, @@ -28,10 +28,10 @@ export function install(plugin) { }); }); }, - out: (value) => { + out: (value): void => { console.log(value); }, - log: (type, params) => { + log: (): void => { }, }); @@ -40,9 +40,9 @@ export function install(plugin) { aiscript.exec(parser.parse(plugin.src)); } -function createPluginEnv(opts) { - const config = new Map(); - for (const [k, v] of Object.entries(opts.plugin.config || {})) { +function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record { + const config = new Map(); + for (const [k, v] of Object.entries(opts.plugin.config ?? {})) { config.set(k, utils.jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default)); } @@ -50,22 +50,28 @@ function createPluginEnv(opts) { ...createAiScriptEnv({ ...opts, token: opts.plugin.token }), //#region Deprecated 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { + utils.assertString(title); registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); }), 'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => { + utils.assertString(title); registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); }), 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { + utils.assertString(title); registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); }), //#endregion 'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => { + utils.assertString(title); registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); }), 'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => { + utils.assertString(title); registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); }), 'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => { + utils.assertString(title); registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); }), 'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => { @@ -75,54 +81,78 @@ function createPluginEnv(opts) { registerNotePostInterruptor({ pluginId: opts.plugin.id, handler }); }), 'Plugin:open_url': values.FN_NATIVE(([url]) => { + utils.assertString(url); window.open(url.value, '_blank'); }), 'Plugin:config': values.OBJ(config), }; } -function initPlugin({ plugin, aiscript }) { +function initPlugin({ plugin, aiscript }): void { pluginContexts.set(plugin.id, aiscript); } -function registerPostFormAction({ pluginId, title, handler }) { +function registerPostFormAction({ pluginId, title, handler }): void { postFormActions.push({ title, handler: (form, update) => { - pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => { - update(key.value, value.value); + const pluginContext = pluginContexts.get(pluginId); + if (!pluginContext) { + return; + } + pluginContext.execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => { + if (!key || !value) { + return; + } + update(utils.valToJs(key), utils.valToJs(value)); })]); }, }); } -function registerUserAction({ pluginId, title, handler }) { +function registerUserAction({ pluginId, title, handler }): void { userActions.push({ title, handler: (user) => { - pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]); + const pluginContext = pluginContexts.get(pluginId); + if (!pluginContext) { + return; + } + pluginContext.execFn(handler, [utils.jsToVal(user)]); }, }); } -function registerNoteAction({ pluginId, title, handler }) { +function registerNoteAction({ pluginId, title, handler }): void { noteActions.push({ title, handler: (note) => { - pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]); + const pluginContext = pluginContexts.get(pluginId); + if (!pluginContext) { + return; + } + pluginContext.execFn(handler, [utils.jsToVal(note)]); }, }); } -function registerNoteViewInterruptor({ pluginId, handler }) { +function registerNoteViewInterruptor({ pluginId, handler }): void { noteViewInterruptors.push({ handler: async (note) => { - return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)])); + const pluginContext = pluginContexts.get(pluginId); + if (!pluginContext) { + return; + } + return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(note)])); }, }); } -function registerNotePostInterruptor({ pluginId, handler }) { +function registerNotePostInterruptor({ pluginId, handler }): void { notePostInterruptors.push({ handler: async (note) => { - return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)])); + const pluginContext = pluginContexts.get(pluginId); + if (!pluginContext) { + return; + } + return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(note)])); }, }); } diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 87d42c5c87..70576688b1 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -3,7 +3,6 @@ import { Router } from '@/nirax'; import { $i, iAmModerator } from '@/account'; import MkLoading from '@/pages/_loading_.vue'; import MkError from '@/pages/_error_.vue'; -import { ui } from '@/config'; const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ loader: loader, @@ -199,8 +198,11 @@ export const routes = [{ component: page(() => import('./pages/theme-editor.vue')), loginRequired: true, }, { - path: '/explore/tags/:tag', - component: page(() => import('./pages/explore.vue')), + path: '/roles/:role', + component: page(() => import('./pages/role.vue')), +}, { + path: '/user-tags/:tag', + component: page(() => import('./pages/user-tag.vue')), }, { path: '/explore', component: page(() => import('./pages/explore.vue')), @@ -211,6 +213,8 @@ export const routes = [{ query: { q: 'query', channel: 'channel', + type: 'type', + origin: 'origin', }, }, { path: '/authorize-follow', @@ -224,9 +228,6 @@ export const routes = [{ path: '/api-console', component: page(() => import('./pages/api-console.vue')), loginRequired: true, -}, { - path: '/mfm-cheat-sheet', - component: page(() => import('./pages/mfm-cheat-sheet.vue')), }, { path: '/scratchpad', component: page(() => import('./pages/scratchpad.vue')), @@ -423,19 +424,6 @@ export const routes = [{ path: '/my/achievements', component: page(() => import('./pages/achievements.vue')), loginRequired: true, -}, { - name: 'messaging', - path: '/my/messaging', - component: page(() => import('./pages/messaging/index.vue')), - loginRequired: true, -}, { - path: '/my/messaging/:userAcct', - component: page(() => import('./pages/messaging/messaging-room.vue')), - loginRequired: true, -}, { - path: '/my/messaging/group/:groupId', - component: page(() => import('./pages/messaging/messaging-room.vue')), - loginRequired: true, }, { path: '/my/drive/folder/:folder', component: page(() => import('./pages/drive.vue')), diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index fb73c0b4b7..6b8041d78e 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -1,4 +1,4 @@ -import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; +import { utils, values } from '@syuilo/aiscript'; import { v4 as uuid } from 'uuid'; import { ref, Ref } from 'vue'; diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index b5d2251d28..9c0ff3d1b2 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -1,6 +1,5 @@ -import { defineAsyncComponent, Ref, inject } from 'vue'; +import { defineAsyncComponent, Ref } from 'vue'; import * as misskey from 'misskey-js'; -import { pleaseLogin } from './please-login'; import { claimAchievement } from './achievements'; import { $i } from '@/account'; import { i18n } from '@/i18n'; @@ -9,8 +8,8 @@ import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import { url } from '@/config'; import { noteActions } from '@/store'; -import { notePage } from '@/filters/note'; import { miLocalStorage } from '@/local-storage'; +import { getUserMenu } from '@/scripts/get-user-menu'; export function getNoteMenu(props: { note: misskey.entities.Note; @@ -101,66 +100,6 @@ export function getNoteMenu(props: { }); } - async function clip(): Promise { - const clips = await os.api('clips/list'); - os.popupMenu([{ - icon: 'ti ti-plus', - text: i18n.ts.createNew, - action: async () => { - const { canceled, result } = await os.form(i18n.ts.createNewClip, { - name: { - type: 'string', - label: i18n.ts.name, - }, - description: { - type: 'string', - required: false, - multiline: true, - label: i18n.ts.description, - }, - isPublic: { - type: 'boolean', - label: i18n.ts.public, - default: false, - }, - }); - if (canceled) return; - - const clip = await os.apiWithDialog('clips/create', result); - - claimAchievement('noteClipped1'); - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); - }, - }, null, ...clips.map(clip => ({ - text: clip.name, - action: () => { - claimAchievement('noteClipped1'); - os.promiseDialog( - os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), - null, - async (err) => { - if (err.id === '734806c4-542c-463a-9311-15c512803965') { - const confirm = await os.confirm({ - type: 'warning', - text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), - }); - if (!confirm.canceled) { - os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); - if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true; - } - } else { - os.alert({ - type: 'error', - text: err.message + '\n' + err.id, - }); - } - }, - ); - }, - }))], props.menuButton.value, { - }).then(focus); - } - async function unclip(): Promise { os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: appearNote.id }); props.isDeleted.value = true; @@ -202,7 +141,7 @@ export function getNoteMenu(props: { props.translating.value = true; const res = await os.api('notes/translate', { noteId: appearNote.id, - targetLang: miLocalStorage.getItem('lang') || navigator.language, + targetLang: miLocalStorage.getItem('lang') ?? navigator.language, }); props.translating.value = false; props.translation.value = res; @@ -242,7 +181,7 @@ export function getNoteMenu(props: { icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { - window.open(appearNote.url || appearNote.uri, '_blank'); + window.open(appearNote.url ?? appearNote.uri, '_blank'); }, } : undefined, { @@ -266,9 +205,67 @@ export function getNoteMenu(props: { action: () => toggleFavorite(true), }), { + type: 'parent', icon: 'ti ti-paperclip', text: i18n.ts.clip, - action: () => clip(), + children: async () => { + const clips = await os.api('clips/list'); + return [{ + icon: 'ti ti-plus', + text: i18n.ts.createNew, + action: async () => { + const { canceled, result } = await os.form(i18n.ts.createNewClip, { + name: { + type: 'string', + label: i18n.ts.name, + }, + description: { + type: 'string', + required: false, + multiline: true, + label: i18n.ts.description, + }, + isPublic: { + type: 'boolean', + label: i18n.ts.public, + default: false, + }, + }); + if (canceled) return; + + const clip = await os.apiWithDialog('clips/create', result); + + claimAchievement('noteClipped1'); + os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); + }, + }, null, ...clips.map(clip => ({ + text: clip.name, + action: () => { + claimAchievement('noteClipped1'); + os.promiseDialog( + os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), + null, + async (err) => { + if (err.id === '734806c4-542c-463a-9311-15c512803965') { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), + }); + if (!confirm.canceled) { + os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); + if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true; + } + } else { + os.alert({ + type: 'error', + text: err.message + '\n' + err.id, + }); + } + }, + ); + }, + }))]; + }, }, statePromise.then(state => state.isMutedThread ? { icon: 'ti ti-message-off', @@ -288,11 +285,20 @@ export function getNoteMenu(props: { text: i18n.ts.pin, action: () => togglePin(true), } : undefined, + appearNote.userId !== $i.id ? { + type: 'parent', + icon: 'ti ti-user', + text: i18n.ts.user, + children: async () => { + const user = await os.api('users/show', { userId: appearNote.userId }); + return getUserMenu(user); + }, + } : undefined, /* ...($i.isModerator || $i.isAdmin ? [ null, { - icon: 'fas fa-bullhorn', + icon: 'ti ti-speakerphone', text: i18n.ts.promote, action: promote }] @@ -304,7 +310,7 @@ export function getNoteMenu(props: { icon: 'ti ti-exclamation-circle', text: i18n.ts.reportAbuse, action: () => { - const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; + const u = appearNote.url ?? appearNote.uri ?? `${url}/notes/${appearNote.id}`; os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: appearNote.user, initialComment: `Note: ${u}\n-----\n`, @@ -346,7 +352,7 @@ export function getNoteMenu(props: { icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { - window.open(appearNote.url || appearNote.uri, '_blank'); + window.open(appearNote.url ?? appearNote.uri, '_blank'); }, } : undefined] .filter(x => x !== undefined); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 74bd61fd78..5170ca4c8c 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -1,5 +1,5 @@ -import * as Acct from 'misskey-js/built/acct'; import { defineAsyncComponent } from 'vue'; +import * as misskey from 'misskey-js'; import { i18n } from '@/i18n'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import { host } from '@/config'; @@ -9,54 +9,9 @@ import { $i, iAmModerator } from '@/account'; import { mainRouter } from '@/router'; import { Router } from '@/nirax'; -export function getUserMenu(user, router: Router = mainRouter) { +export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) { const meId = $i ? $i.id : null; - async function pushList() { - const t = i18n.ts.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく - const lists = await os.api('users/lists/list'); - if (lists.length === 0) { - os.alert({ - type: 'error', - text: i18n.ts.youHaveNoLists, - }); - return; - } - const { canceled, result: listId } = await os.select({ - title: t, - items: lists.map(list => ({ - value: list.id, text: list.name, - })), - }); - if (canceled) return; - os.apiWithDialog('users/lists/push', { - listId: listId, - userId: user.id, - }); - } - - async function inviteGroup() { - const groups = await os.api('users/groups/owned'); - if (groups.length === 0) { - os.alert({ - type: 'error', - text: i18n.ts.youHaveNoGroups, - }); - return; - } - const { canceled, result: groupId } = await os.select({ - title: i18n.ts.group, - items: groups.map(group => ({ - value: group.id, text: group.name, - })), - }); - if (canceled) return; - os.apiWithDialog('users/groups/invite', { - groupId: groupId, - userId: user.id, - }); - } - async function toggleMute() { if (user.isMuted) { os.apiWithDialog('mute/delete', { @@ -125,6 +80,8 @@ export function getUserMenu(user, router: Router = mainRouter) { } async function invalidateFollow() { + if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return; + os.apiWithDialog('following/invalidate', { userId: user.id, }).then(() => { @@ -136,7 +93,7 @@ export function getUserMenu(user, router: Router = mainRouter) { icon: 'ti ti-at', text: i18n.ts.copyUsername, action: () => { - copyToClipboard(`@${user.username}@${user.host || host}`); + copyToClipboard(`@${user.username}@${user.host ?? host}`); }, }, { icon: 'ti ti-info-circle', @@ -156,22 +113,68 @@ export function getUserMenu(user, router: Router = mainRouter) { action: () => { os.post({ specified: user }); }, - }, meId !== user.id ? { - type: 'link', - icon: 'ti ti-messages', - text: i18n.ts.startMessaging, - to: '/my/messaging/' + Acct.toString(user), - } : undefined, null, { + }, null, { + type: 'parent', icon: 'ti ti-list', text: i18n.ts.addToList, - action: pushList, - }, meId !== user.id ? { - icon: 'ti ti-users', - text: i18n.ts.inviteToGroup, - action: inviteGroup, - } : undefined] as any; + children: async () => { + const lists = await os.api('users/lists/list'); + + return lists.map(list => ({ + text: list.name, + action: () => { + os.apiWithDialog('users/lists/push', { + listId: list.id, + userId: user.id, + }); + }, + })); + }, + }] as any; if ($i && meId !== user.id) { + if (iAmModerator) { + menu = menu.concat([{ + type: 'parent', + icon: 'ti ti-badges', + text: i18n.ts.roles, + children: async () => { + const roles = await os.api('admin/roles/list'); + + return roles.filter(r => r.target === 'manual').map(r => ({ + text: r.name, + action: async () => { + const { canceled, result: period } = await os.select({ + title: i18n.ts.period, + items: [{ + value: 'indefinitely', text: i18n.ts.indefinitely, + }, { + value: 'oneHour', text: i18n.ts.oneHour, + }, { + value: 'oneDay', text: i18n.ts.oneDay, + }, { + value: 'oneWeek', text: i18n.ts.oneWeek, + }, { + value: 'oneMonth', text: i18n.ts.oneMonth, + }], + default: 'indefinitely', + }); + if (canceled) return; + + const expiresAt = period === 'indefinitely' ? null + : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) + : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) + : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) + : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) + : null; + + os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt }); + }, + })); + }, + }]); + } + menu = menu.concat([null, { icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, @@ -195,16 +198,6 @@ export function getUserMenu(user, router: Router = mainRouter) { text: i18n.ts.reportAbuse, action: reportAbuse, }]); - - if (iAmModerator) { - menu = menu.concat([null, { - icon: 'ti ti-user-exclamation', - text: i18n.ts.moderation, - action: () => { - router.push('/user-info/' + user.id + '#moderation'); - }, - }]); - } } if ($i && meId === user.id) { diff --git a/packages/frontend/src/scripts/get-user-name.ts b/packages/frontend/src/scripts/get-user-name.ts index d499ea0203..4daf203e06 100644 --- a/packages/frontend/src/scripts/get-user-name.ts +++ b/packages/frontend/src/scripts/get-user-name.ts @@ -1,3 +1,3 @@ export default function(user: { name?: string | null, username: string }): string { - return user.name || user.username; + return user.name === '' ? user.username : user.name ?? user.username; } diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts index 4a0ded637d..b7238016c6 100644 --- a/packages/frontend/src/scripts/hotkey.ts +++ b/packages/frontend/src/scripts/hotkey.ts @@ -53,10 +53,10 @@ const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, c return result; }); -const ignoreElemens = ['input', 'textarea']; +const ignoreElements = ['input', 'textarea']; function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean { - const key = ev.code.toLowerCase(); + const key = ev.key.toLowerCase(); return patterns.some(pattern => pattern.which.includes(key) && pattern.ctrl === ev.ctrlKey && pattern.shift === ev.shiftKey && @@ -70,7 +70,7 @@ export const makeHotkey = (keymap: Keymap) => { return (ev: KeyboardEvent) => { if (document.activeElement) { - if (ignoreElemens.some(el => document.activeElement!.matches(el))) return; + if (ignoreElements.some(el => document.activeElement!.matches(el))) return; if (document.activeElement.attributes['contenteditable']) return; } diff --git a/packages/frontend/src/scripts/hpml/evaluator.ts b/packages/frontend/src/scripts/hpml/evaluator.ts index d4090ea15c..7bddd3f62d 100644 --- a/packages/frontend/src/scripts/hpml/evaluator.ts +++ b/packages/frontend/src/scripts/hpml/evaluator.ts @@ -1,11 +1,10 @@ import autobind from 'autobind-decorator'; -import { markRaw, ref, Ref, unref } from 'vue'; +import { ref, Ref, unref } from 'vue'; import { collectPageVars } from '../collect-page-vars'; -import { initHpmlLib, initAiLib } from './lib'; +import { initHpmlLib } from './lib'; import { Expr, isLiteralValue, Variable } from './expr'; import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.'; import { version } from '@/config'; -import * as os from '@/os'; /** * Hpml evaluator diff --git a/packages/frontend/src/scripts/hpml/index.ts b/packages/frontend/src/scripts/hpml/index.ts index 9a55a5c286..587c6a36c8 100644 --- a/packages/frontend/src/scripts/hpml/index.ts +++ b/packages/frontend/src/scripts/hpml/index.ts @@ -15,12 +15,12 @@ export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null; export const literalDefs: Record = { text: { out: 'string', category: 'value', icon: 'ti ti-quote' }, - multiLineText: { out: 'string', category: 'value', icon: 'fas fa-align-left' }, - textList: { out: 'stringArray', category: 'value', icon: 'fas fa-list' }, - number: { out: 'number', category: 'value', icon: 'fas fa-sort-numeric-up' }, - ref: { out: null, category: 'value', icon: 'fas fa-magic' }, - aiScriptVar: { out: null, category: 'value', icon: 'fas fa-magic' }, - fn: { out: 'function', category: 'value', icon: 'fas fa-square-root-alt' }, + multiLineText: { out: 'string', category: 'value', icon: 'ti ti-align-left' }, + textList: { out: 'stringArray', category: 'value', icon: 'ti ti-list' }, + number: { out: 'number', category: 'value', icon: 'ti ti-list-numbers' }, + ref: { out: null, category: 'value', icon: 'ti ti-wand' }, + aiScriptVar: { out: null, category: 'value', icon: 'ti ti-wand' }, + fn: { out: 'function', category: 'value', icon: 'ti ti-math-function' }, }; export const blockDefs = [ @@ -58,7 +58,7 @@ export class HpmlScope { constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) { this.layerdStates = layerdStates; - this.name = name || 'anonymous'; + this.name = name ?? 'anonymous'; } @autobind diff --git a/packages/frontend/src/scripts/hpml/lib.ts b/packages/frontend/src/scripts/hpml/lib.ts index 02d663b31b..88db82dd27 100644 --- a/packages/frontend/src/scripts/hpml/lib.ts +++ b/packages/frontend/src/scripts/hpml/lib.ts @@ -1,4 +1,3 @@ -import tinycolor from 'tinycolor2'; import seedrandom from 'seedrandom'; import { Hpml } from './evaluator'; import { Expr } from './expr'; @@ -130,42 +129,42 @@ export function initAiLib(hpml: Hpml) { export const funcDefs: Record = { if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'ti ti-share' }, - for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'fas fa-recycle' }, - not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, - or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, - and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, + for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'ti ti-recycle' }, + not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'ti ti-flag' }, + or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'ti ti-flag' }, + and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'ti ti-flag' }, add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-plus' }, subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-minus' }, multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-x' }, - divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' }, - mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' }, - round: { in: ['number'], out: 'number', category: 'operation', icon: 'fas fa-calculator' }, - eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-equals' }, - notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-not-equal' }, - gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than' }, - lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than' }, - gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than-equal' }, - ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than-equal' }, + divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-divide' }, + mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-divide' }, + round: { in: ['number'], out: 'number', category: 'operation', icon: 'ti ti-calculator' }, + eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'ti ti-equal' }, + notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'ti ti-equal-not' }, + gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-greater' }, + lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-lower' }, + gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-equal-greater' }, + ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'ti ti-math-equal-lower' }, strLen: { in: ['string'], out: 'number', category: 'text', icon: 'ti ti-quote' }, strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'ti ti-quote' }, strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'fas fa-exchange-alt' }, - numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'fas fa-exchange-alt' }, - splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'fas fa-exchange-alt' }, - pick: { in: [null, 'number'], out: null, category: 'list', icon: 'fas fa-indent' }, - listLen: { in: [null], out: 'number', category: 'list', icon: 'fas fa-indent' }, - rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, - dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, - seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, - random: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, - dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, - seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, - randomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' }, - dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' }, - seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'fas fa-dice' }, - DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'fas fa-dice' }, // dailyRandomPickWithProbabilityMapping + stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'ti ti-arrows-right-left' }, + numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'ti ti-arrows-right-left' }, + splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'ti ti-arrows-right-left' }, + pick: { in: [null, 'number'], out: null, category: 'list', icon: 'ti ti-indent-increase' }, + listLen: { in: [null], out: 'number', category: 'list', icon: 'ti ti-indent-increase' }, + rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'ti ti-dice' }, + dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'ti ti-dice' }, + seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'ti ti-dice' }, + random: { in: ['number'], out: 'boolean', category: 'random', icon: 'ti ti-dice' }, + dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'ti ti-dice' }, + seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'ti ti-dice' }, + randomPick: { in: [0], out: 0, category: 'random', icon: 'ti ti-dice' }, + dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'ti ti-dice' }, + seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'ti ti-dice' }, + DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'ti ti-dice' }, // dailyRandomPickWithProbabilityMapping }; export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) { diff --git a/packages/frontend/src/scripts/hpml/type-checker.ts b/packages/frontend/src/scripts/hpml/type-checker.ts index 24c9ed8bcb..692826fc90 100644 --- a/packages/frontend/src/scripts/hpml/type-checker.ts +++ b/packages/frontend/src/scripts/hpml/type-checker.ts @@ -63,7 +63,7 @@ export class HpmlTypeChecker { @autobind public getExpectedType(v: Expr, slot: number): Type { - const def = funcDefs[v.type || '']; + const def = funcDefs[v.type ?? '']; if (def == null) { throw new Error('Unknown type: ' + v.type); } @@ -107,7 +107,7 @@ export class HpmlTypeChecker { return pageVar.type; } - const envVar = envVarsDef[v.value || '']; + const envVar = envVarsDef[v.value ?? '']; if (envVar !== undefined) { return envVar; } diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts index 69f6a82803..35813edbd5 100644 --- a/packages/frontend/src/scripts/keycode.ts +++ b/packages/frontend/src/scripts/keycode.ts @@ -16,18 +16,3 @@ export const aliases = { 'right': 'ArrowRight', 'plus': ['NumpadAdd', 'Semicolon'], }; - -/*! -* Programmatically add the following -*/ - -// lower case chars -for (let i = 97; i < 123; i++) { - const char = String.fromCharCode(i); - aliases[char] = `Key${char.toUpperCase()}`; -} - -// numbers -for (let i = 0; i < 10; i++) { - aliases[i] = [`Numpad${i}`, `Digit${i}`]; -} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 274e96e0a1..2fe5bdcf8f 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -1,20 +1,20 @@ -import { query, appendQuery } from '@/scripts/url'; +import { query } from '@/scripts/url'; import { url } from '@/config'; import { instance } from '@/instance'; -export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string { - if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/')) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - return appendQuery(imageUrl, query({ - fallback: '1', - ...(type ? { [type]: '1' } : {}), - })); +export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigin: boolean = false): string { + const localProxy = `${url}/proxy`; + + if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { + // もう既にproxyっぽそうだったらurlを取り出す + imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; } - return `${instance.mediaProxy}/image.webp?${query({ + return `${mustOrigin ? localProxy : instance.mediaProxy}/image.webp?${query({ url: imageUrl, fallback: '1', ...(type ? { [type]: '1' } : {}), + ...(mustOrigin ? { origin: '1' } : {}), })}`; } diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/scripts/page-metadata.ts index 0db8369f9d..8810e26960 100644 --- a/packages/frontend/src/scripts/page-metadata.ts +++ b/packages/frontend/src/scripts/page-metadata.ts @@ -10,7 +10,6 @@ export type PageMetadata = { icon?: string | null; avatar?: misskey.entities.User | null; userName?: misskey.entities.User | null; - bg?: string; }; export function definePageMetadata(metadata: PageMetadata | null | Ref | ComputedRef): void { diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts index e84eebf103..cb45002202 100644 --- a/packages/frontend/src/scripts/popup-position.ts +++ b/packages/frontend/src/scripts/popup-position.ts @@ -1,4 +1,3 @@ -import { Ref } from 'vue'; export function calcPopupPosition(el: HTMLElement, props: { anchorElement: HTMLElement | null; diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts index e3d9dc00c2..a002f02b5a 100644 --- a/packages/frontend/src/scripts/scroll.ts +++ b/packages/frontend/src/scripts/scroll.ts @@ -10,7 +10,7 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { } } -export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top: number = 0) { +export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top = 0) { if (!el.parentElement) return top; const data = el.dataset.stickyContainerHeaderHeight; const newTop = data ? Number(data) + top : top; @@ -23,14 +23,14 @@ export function getScrollPosition(el: HTMLElement | null): number { return container == null ? window.scrollY : container.scrollTop; } -export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) { +export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { // とりあえず評価してみる if (isTopVisible(el)) { cb(); if (once) return null; } - const container = getScrollContainer(el) || window; + const container = getScrollContainer(el) ?? window; const onScroll = ev => { if (!document.body.contains(el)) return; @@ -45,7 +45,7 @@ export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance: numbe return removeListener; } -export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) { +export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { const container = getScrollContainer(el); // とりあえず評価してみる @@ -54,7 +54,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance: nu if (once) return null; } - const containerOrWindow = container || window; + const containerOrWindow = container ?? window; const onScroll = ev => { if (!document.body.contains(el)) return; if (isBottomVisible(el, 1, container)) { @@ -104,12 +104,12 @@ export function scrollToBottom( } else { window.scroll({ top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0, - ...options + ...options, }); } } -export function isTopVisible(el: HTMLElement, tolerance: number = 1): boolean { +export function isTopVisible(el: HTMLElement, tolerance = 1): boolean { const scrollTop = getScrollPosition(el); return scrollTop <= tolerance; } @@ -124,6 +124,6 @@ export function getBodyScrollHeight() { return Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, - document.body.clientHeight, document.documentElement.clientHeight + document.body.clientHeight, document.documentElement.clientHeight, ); } diff --git a/packages/frontend/src/scripts/search.ts b/packages/frontend/src/scripts/search.ts deleted file mode 100644 index 64914d3d65..0000000000 --- a/packages/frontend/src/scripts/search.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as os from '@/os'; -import { i18n } from '@/i18n'; -import { mainRouter } from '@/router'; - -export async function search() { - const { canceled, result: query } = await os.inputText({ - title: i18n.ts.search, - }); - if (canceled || query == null || query === '') return; - - const q = query.trim(); - - if (q.startsWith('@') && !q.includes(' ')) { - mainRouter.push(`/${q}`); - return; - } - - if (q.startsWith('#')) { - mainRouter.push(`/tags/${encodeURIComponent(q.substr(1))}`); - return; - } - - // like 2018/03/12 - if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) { - const date = new Date(q.replace(/-/g, '/')); - - // 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは - // 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので - // 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の - // 結果になってしまい、2018/03/12 のコンテンツは含まれない) - if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) { - date.setHours(23, 59, 59, 999); - } - - // TODO - //v.$root.$emit('warp', date); - os.alert({ - icon: 'fas fa-history', - iconOnly: true, autoClose: true, - }); - return; - } - - if (q.startsWith('https://')) { - const promise = os.api('ap/show', { - uri: q, - }); - - os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); - - const res = await promise; - - if (res.type === 'User') { - mainRouter.push(`/@${res.object.username}@${res.object.host}`); - } else if (res.type === 'Note') { - mainRouter.push(`/notes/${res.object.id}`); - } - - return; - } - - mainRouter.push(`/search?q=${encodeURIComponent(q)}`); -} diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 3468a0e4b5..3c05da6827 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -4,6 +4,27 @@ const cache = new Map(); export const soundsTypes = [ null, + 'syuilo/n-aec', + 'syuilo/n-aec-4va', + 'syuilo/n-aec-4vb', + 'syuilo/n-aec-8va', + 'syuilo/n-aec-8vb', + 'syuilo/n-cea', + 'syuilo/n-cea-4va', + 'syuilo/n-cea-4vb', + 'syuilo/n-cea-8va', + 'syuilo/n-cea-8vb', + 'syuilo/n-eca', + 'syuilo/n-eca-4va', + 'syuilo/n-eca-4vb', + 'syuilo/n-eca-8va', + 'syuilo/n-eca-8vb', + 'syuilo/n-ea', + 'syuilo/n-ea-4va', + 'syuilo/n-ea-4vb', + 'syuilo/n-ea-8va', + 'syuilo/n-ea-8vb', + 'syuilo/n-ea-harmony', 'syuilo/up', 'syuilo/down', 'syuilo/pope1', diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend/src/scripts/use-document-visibility.ts new file mode 100644 index 0000000000..47e91dd937 --- /dev/null +++ b/packages/frontend/src/scripts/use-document-visibility.ts @@ -0,0 +1,19 @@ +import { onMounted, onUnmounted, ref, Ref } from 'vue'; + +export function useDocumentVisibility(): Ref { + const visibility = ref(document.visibilityState); + + const onChange = (): void => { + visibility.value = document.visibilityState; + }; + + onMounted(() => { + document.addEventListener('visibilitychange', onChange); + }); + + onUnmounted(() => { + document.removeEventListener('visibilitychange', onChange); + }); + + return visibility; +} diff --git a/packages/frontend/src/scripts/use-leave-guard.ts b/packages/frontend/src/scripts/use-leave-guard.ts index a93b84d1fe..146b012471 100644 --- a/packages/frontend/src/scripts/use-leave-guard.ts +++ b/packages/frontend/src/scripts/use-leave-guard.ts @@ -1,6 +1,4 @@ -import { inject, onUnmounted, Ref } from 'vue'; -import { i18n } from '@/i18n'; -import * as os from '@/os'; +import { Ref } from 'vue'; export function useLeaveGuard(enabled: Ref) { /* TODO diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 94892ff526..a6ad1774ff 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -1,6 +1,5 @@ import { markRaw, ref } from 'vue'; import { Storage } from './pizzax'; -import { Theme } from './scripts/theme'; interface PostFormAction { title: string, @@ -46,6 +45,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: false, }, + collapseRenotes: { + where: 'account', + default: true, + }, rememberNoteVisibility: { where: 'account', default: false, @@ -158,6 +161,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + advancedMfm: { + where: 'device', + default: true, + }, loadRawImages: { where: 'device', default: false, @@ -266,6 +273,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 5, }, + showNoteActionsOnlyHover: { + where: 'device', + default: false, + }, aiChanMode: { where: 'device', default: false, @@ -276,12 +287,15 @@ export const defaultStore = markRaw(new Storage('base', { const PREFIX = 'miux:' as const; -type Plugin = { +export type Plugin = { id: string; name: string; active: boolean; + config?: Record; configData: Record; token: string; + src: string | null; + version: string; ast: any[]; }; @@ -305,14 +319,14 @@ export class ColdDeviceStorage { syncDeviceDarkMode: true, plugins: [] as Plugin[], mediaVolume: 0.5, - sound_masterVolume: 0.3, - sound_note: { type: 'syuilo/down', volume: 1 }, - sound_noteMy: { type: 'syuilo/up', volume: 1 }, - sound_notification: { type: 'syuilo/pope2', volume: 1 }, - sound_chat: { type: 'syuilo/pope1', volume: 1 }, - sound_chatBg: { type: 'syuilo/waon', volume: 1 }, - sound_antenna: { type: 'syuilo/triple', volume: 1 }, - sound_channel: { type: 'syuilo/square-pico', volume: 1 }, + sound_masterVolume: 0.5, + sound_note: { type: 'syuilo/n-aec', volume: 0.5 }, + sound_noteMy: { type: 'syuilo/n-cea', volume: 0.5 }, + sound_notification: { type: 'syuilo/n-ea', volume: 0.5 }, + sound_chat: { type: 'syuilo/pope1', volume: 0.5 }, + sound_chatBg: { type: 'syuilo/waon', volume: 0.5 }, + sound_antenna: { type: 'syuilo/triple', volume: 0.5 }, + sound_channel: { type: 'syuilo/square-pico', volume: 0.5 }, }; public static watchers: Watcher[] = []; diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts index aa1244665b..580c7da007 100644 --- a/packages/frontend/src/theme-store.ts +++ b/packages/frontend/src/theme-store.ts @@ -1,13 +1,13 @@ -import { api } from '@/os'; -import { $i } from '@/account'; import { Theme } from './scripts/theme'; import { miLocalStorage } from './local-storage'; +import { api } from '@/os'; +import { $i } from '@/account'; const lsCacheKey = $i ? `themes:${$i.id}` as const : null; export function getThemes(): Theme[] { if ($i == null) return []; - return JSON.parse(miLocalStorage.getItem(lsCacheKey!) || '[]'); + return JSON.parse(miLocalStorage.getItem(lsCacheKey!) ?? '[]'); } export async function fetchThemes(): Promise { diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 342b3f2dbf..976345f9ee 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -30,11 +30,11 @@

a c d